Зачем вам может понадобиться имя класса в рантайме?
Представьте: вы пишете сложную систему на TypeScript. У вас есть иерархия классов — User, Admin, Moderator. Где-то в глубине кода вы получаете объект, но не знаете, кто он на самом деле. Вы видите только object в консоли.
Или вы строите логгер, который должен выводить не просто [Object], а осмысленное имя класса: [User: John]. Или вы пишете декоратор, который должен решать, как обрабатывать объект, в зависимости от его типа.
Вот тут-то и приходит на помощь вопрос с тысячью просмотров на Stack Overflow: "Get an object's class name at runtime". Сегодня мы не просто дадим ответ, а заглянем под капот TypeScript и JavaScript, чтобы понять, как это работает на самом деле.
Да, TypeScript — это надстройка над JavaScript. Вся магия типов исчезает после компиляции. Но имя класса — это не тип, это свойство прототипа. И оно остаётся с нами в рантайме.
Самый простой способ: constructor.name
Если вы когда-нибудь гуглили этот вопрос, вы наверняка видели этот однострочник:
class User {
constructor(public name: string) {}
}
const user = new User("Алиса");
console.log(user.constructor.name); // "User"
Работает? Да. Быстро? Да. Но есть нюансы, которые могут вас укусить, если не знать, как это устроено.
Как это работает под капотом?
Каждый объект в JavaScript имеет свойство constructor. Это ссылка на функцию, которая создала этот объект. А у любой функции (класс — это тоже функция) есть свойство name.
Когда вы пишете user.constructor.name, движок делает следующее:
- Идёт по цепочке прототипов объекта
user. - Находит свойство
constructorнаUser.prototype. - Читает
nameу функции-конструктораUser.
Это дешёвая операция, но она не бесплатна. В большинстве случаев вы этого даже не заметите.
Когда constructor.name не сработает?
Вот классический грабль:
const obj = Object.create(null);
console.log(obj.constructor); // undefined
// console.log(obj.constructor.name); // TypeError!
Объект, созданный через Object.create(null), не имеет прототипа. У него нет constructor. Будьте осторожны.
Второй случай — минификация. Если вы используете сборщик (Webpack, Terser), который переименовывает классы, constructor.name может вернуть что-то вроде t или e. Это проблема только для продакшена, но о ней стоит знать.
Альтернатива: instanceof и кастомные проверки
Иногда вам нужно не просто имя, а проверка: "Является ли этот объект экземпляром класса X?"
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
instanceof работает через цепочку прототипов. Он проверяет, есть ли Dog.prototype в цепочке прототипов объекта dog. Если да — true.
Но instanceof не даёт вам строку с именем. Он даёт булево значение. Для логов или сериализации это не подходит.
Комбинируем: проверка + имя
Вы можете написать утилиту, которая сначала проверяет тип, а потом возвращает имя:
function getClassName(obj: any): string | null {
if (obj === null || obj === undefined) return null;
if (typeof obj !== 'object' && typeof obj !== 'function') return null;
// Защита от Object.create(null)
if (!obj.constructor) return 'Object (no prototype)';
const name = obj.constructor.name;
return name || 'anonymous class';
}
class MyClass {}
console.log(getClassName(new MyClass())); // "MyClass"
console.log(getClassName({})); // "Object"
console.log(getClassName(null)); // null
Этот код уже более надёжный. Он обрабатывает краевые случаи и возвращает осмысленные значения.
TypeScript: типы не помогают в рантайме
Важно понимать: TypeScript — это про время компиляции. В рантайме ваш код — это чистый JavaScript. Все аннотации типов, интерфейсы, дженерики — стираются.
// TypeScript
class Box<T> {
constructor(public value: T) {}
}
const box = new Box<number>(42);
console.log(box.constructor.name); // "Box" — тип T потерян
Вы не сможете получить number из Box<number> в рантайме. Если вам нужно сохранить информацию о типе, используйте дискриминаторы, декораторы или явные поля.
Дискриминатор — простой и надёжный
class User {
readonly kind = 'User'; // дискриминатор
constructor(public name: string) {}
}
class Admin {
readonly kind = 'Admin';
constructor(public name: string) {}
}
function greet(obj: User | Admin) {
if (obj.kind === 'User') {
console.log(`Привет, пользователь ${obj.name}`);
} else {
console.log(`Привет, админ ${obj.name}`);
}
}
Это работает и в TypeScript (для сужения типов), и в рантайме (для логики). Минус — нужно явно прописывать kind в каждом классе.
Продвинутый уровень: декораторы и метаданные
Если вы пишете на TypeScript с включёнными декораторами (experimentalDecorators), вы можете автоматически добавить имя класса в прототип или в метаданные.
function Loggable(target: any) {
// target — это конструктор класса
target.prototype.className = target.name;
}
@Loggable
class Service {
doSomething() {}
}
const service = new Service();
console.log((service as any).className); // "Service"
Это уже похоже на рефлексию. Но помните: декораторы — экспериментальная фича. В будущем стандарт может измениться.
Более современный подход — использовать Symbol.metadata или Reflect.metadata, но это тема для отдельной статьи.
Практический пример: универсальный логгер
Давайте соберём всё вместе и напишем полезную функцию, которая логирует объекты с их именами классов.
function logObject(obj: any, message?: string): void {
const className = getClassName(obj);
const timestamp = new Date().toISOString();
const msg = message ? `: ${message}` : '';
console.log(`[${timestamp}] [${className}]${msg}`, obj);
}
// Использование
class Order {
constructor(public id: number, public total: number) {}
}
const order = new Order(101, 2500);
logObject(order, 'Новый заказ');
// [202