Как получить имя класса объекта в TypeScript во время выполнения?
Проблема с constructor.name и её причины
Вы правы — использование obj.constructor.name в TypeScript действительно ненадёжно. Основная причина кроется не в самом TypeScript, а в процессе сборки JavaScript. Современные бандлеры (Webpack, Rollup, esbuild) и минификаторы (Terser) часто изменяют имена классов для уменьшения размера кода. Например, класс Dog может превратиться в a или t. Кроме того, TypeScript может генерировать код, где constructor.name ведёт себя неожиданно при наследовании, если не соблюдать определённые правила.
Лучшее решение: статическое свойство класса
Самый надёжный способ — определить в классе статическое свойство с именем вручную. Это гарантирует, что имя сохранится независимо от сборки и минификации. TypeScript позволяет сделать это элегантно:
class Animal {
public static className = 'Animal';
name: string;
constructor(name: string) {
this.name = name;
}
getClassName(): string {
return (this.constructor as any).className;
}
}
class Dog extends Animal {
public static className = 'Dog';
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
const myPet = new Dog('Rex', 'Labrador');
console.log(myPet.getClassName()); // Всегда "Dog"
console.log(Dog.className); // "Dog"
Почему это работает?
- Статические свойства наследуются — если вы не переопределите
classNameв дочернем классе, он унаследует значение от родителя. Поэтому важно задавать его в каждом классе явно. - Не зависит от сборки — строки не меняются при минификации.
- Работает в рантайме — вы можете получить имя как через экземпляр, так и через сам класс.
Улучшенная версия с декоратором (для продвинутых)
Если у вас много классов и вы не хотите писать статическое свойство вручную, можно создать декоратор. Учтите, что для использования декораторов нужно включить "experimentalDecorators": true в tsconfig.json:
function ClassName(name: string) {
return function (target: any) {
target.className = name;
};
}
@ClassName('Animal')
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
@ClassName('Dog')
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
function getClassInstanceName(instance: any): string {
return instance.constructor.className || instance.constructor.name;
}
const myPet = new Dog('Rex', 'Labrador');
console.log(getClassInstanceName(myPet)); // "Dog"
Альтернатива: Symbol для уникальности
Если вам нужно не просто имя, а уникальный идентификатор класса (например, для сериализации), можно использовать Symbol:
class Animal {
static classId = Symbol('Animal');
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
static classId = Symbol('Dog');
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
function getClassId(instance: any): Symbol | undefined {
return instance.constructor.classId;
}
const myPet = new Dog('Rex', 'Labrador');
console.log(getClassId(myPet) === Dog.classId); // true
Что делать с constructor.name?
Если вы уверены, что ваш код не будет минифицирован (например, только для Node.js сервера без сборки), можно использовать constructor.name, но с проверкой:
function getSafeClassName(obj: any): string {
return obj?.constructor?.name || 'Unknown';
}
Но для продакшена это рискованно. Рекомендую всегда использовать первый подход со статическим свойством — он простой, понятный и работает везде.
<<Использование TypeScript Map для реестра классов
Ещё один подход, который часто применяют в крупных проектах — создание реестра классов. Он особенно полезен, когда нужно не просто получить имя, а восстановить объект из JSON (десериализация).
// Реестр классов
const classRegistry = new Map<string, new (...args: any[]) => any>();
function registerClass(name: string, cls: new (...args: any[]) => any) {
classRegistry.set(name, cls);
}
// Декоратор для автоматической регистрации
function Registered(name: string) {
return function (target: any) {
registerClass(name, target);
};
}
@Registered('Animal')
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
@Registered('Dog')
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
}
// Функция получения имени через реестр
function getRegisteredClassName(instance: any): string | undefined {
for (const [name, cls] of classRegistry) {
if (instance instanceof cls) {
return name;
}
}
return undefined;
}
const myPet = new Dog('Rex', 'Labrador');
console.log(getRegisteredClassName(myPet)); // "Dog"
// Восстановление объекта из JSON
function fromJSON(json: string): any {
const data = JSON.parse(json);
const cls = classRegistry.get(data._className);
if (!cls) throw new Error('Unknown class');
return Object.assign(new cls(), data);
}
const serialized = JSON.stringify({ _className: 'Dog', name: 'Rex', breed: 'Labrador' });
const restored = fromJSON(serialized);
console.log(restored instanceof Dog); // true
Этот подход решает не только задачу получения имени класса, но и даёт мощный механизм для сериализации/десериализации. Минус — нужно явно регистрировать каждый класс, но это окупается в больших проектах с сложной иерархией.