Как получить имя класса объекта во время выполнения в TypeScript?
Проблема с constructor.name и её решение
Вы абсолютно правы: полагаться на obj.constructor.name в production-коде — рискованно. Современные сборщики (Webpack, esbuild, Terser) часто переименовывают классы или удаляют свойство name из прототипов для уменьшения размера бандла. Есть несколько надёжных способов решить эту задачу.
Способ 1: Явное статическое свойство (рекомендуемый)
Самый простой и надёжный способ — добавить в каждый класс статическое поле с именем. TypeScript позволяет это делать красиво с помощью абстрактных классов:
abstract class Shape {
static className: string = 'Shape';
abstract getClassName(): string;
}
class Circle extends Shape {
static className: string = 'Circle';
getClassName(): string {
return Circle.className;
}
}
class Rectangle extends Shape {
static className: string = 'Rectangle';
getClassName(): string {
return Rectangle.className;
}
}
function printShapeType(shape: Shape): void {
console.log(shape.getClassName());
}
const shapes: Shape[] = [new Circle(10), new Rectangle(5, 7)];
shapes.forEach(printShapeType);
// Вывод: "Circle", "Rectangle"
Плюсы:
- Работает даже при полной обфускации кода
- Типобезопасно — TypeScript следит за реализацией метода
- Не требует рефлексии
Способ 2: Декоратор класса (для продвинутых)
Если у вас много классов и не хочется писать getClassName() вручную, используйте декоратор:
function ClassName(name: string) {
return function (constructor: Function) {
constructor.prototype.__className = name;
};
}
@ClassName('Circle')
class Circle extends Shape {
constructor(public radius: number) {
super('Circle');
}
}
@ClassName('Rectangle')
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super('Rectangle');
}
}
// Вспомогательный геттер
function getClassName(obj: any): string {
return obj.__className || obj.constructor.name;
}
console.log(getClassName(new Circle(10))); // "Circle"
Важно: Для работы декораторов в tsconfig.json нужно включить "experimentalDecorators": true.
Способ 3: Map с конструкторами (если классы известны заранее)
Если у вас фиксированный набор классов, можно создать явный реестр:
const classRegistry = new Map<Function, string>([
[Circle, 'Circle'],
[Rectangle, 'Rectangle'],
]);
function getClassName(shape: Shape): string {
return classRegistry.get(shape.constructor) ?? 'Unknown';
}
Этот способ тоже устойчив к минификации, так как мы храним ссылки на сами конструкторы, а не на их имена.
Что выбрать для вашей задачи?
Для сериализации я рекомендую первый способ (статическое свойство + абстрактный метод). Он:
- Не требует дополнительных настроек компилятора
- Работает с любыми сборщиками
- Даёт полный контроль над именем класса
- Легко тестируется
Если вы пишете библиотеку и хотите дать пользователям возможность кастомизировать имена — используйте декораторы. Если у вас legacy-код, где нельзя менять классы — Map с конструкторами будет наименее инвазивным решением.
Помните: instanceof отлично подходит для проверки типа, но не для получения строкового имени. Комбинируйте эти подходы по необходимости.
Альтернативный подход: Symbol и дескрипторы
Если вам не нравится загромождать классы статическими полями, можно использовать Symbol для хранения имени класса в прототипе. Это скрывает детали реализации от пользователей класса.
const CLASS_NAME = Symbol('className');
function setClassName(target: any, name: string) {
Object.defineProperty(target.prototype, CLASS_NAME, {
value: name,
writable: false,
enumerable: false,
configurable: false
});
}
class Shape {
constructor() {
setClassName(this.constructor, 'Shape');
}
}
class Circle extends Shape {
constructor() {
super();
setClassName(this.constructor, 'Circle');
}
}
function getRuntimeClassName(obj: any): string {
return obj[CLASS_NAME] ?? 'Unknown';
}
console.log(getRuntimeClassName(new Circle())); // "Circle"
Преимущества:
- Имя класса не "засвечивается" в обычных перечислениях свойств
- Легко убрать из production-сборки через tree-shaking, если не используется
- Не требует изменения сигнатуры классов
Недостаток: Нужно не забывать вызывать setClassName в каждом конструкторе наследника. Это можно автоматизировать через вызов super() и проверку в базовом классе, но код станет сложнее. Для небольших проектов этот метод удобнее, чем декораторы, так как не требует включения экспериментальных возможностей TypeScript.
Ответов пока нет.
Войдите чтобы оставить ответ