Avatar ·

Как получить имя класса объекта во время выполнения в TypeScript?

Привет! Я пишу приложение на TypeScript, которое занимается сериализацией разных сущностей. У меня есть иерархия классов, и мне нужно во время выполнения программы определить, экземпляром какого именно класса является объект, чтобы корректно его обработать. Например, у меня есть базовый класс `Shape` и два наследника: `Circle` и `Rectangle`. Я хочу написать функцию, которая принимает массив фигур и для каждой выводит её тип. В JavaScript я бы использовал `obj.constructor.name`, но в TypeScript это свойство может отсутствовать или быть неопределённым после минификации/сборки. К тому же, TypeScript строго типизирован, и просто так обратиться к `constructor` не всегда даёт ожидаемый результат. Вот упрощённый код, который не работает так, как я ожидаю: ```typescript class Shape { constructor(public name: string) {} } class Circle extends Shape { constructor(public radius: number) { super('Circle'); } } class Rectangle extends Shape { constructor(public width: number, public height: number) { super('Rectangle'); } } function printShapeType(shape: Shape): void { // Как здесь получить строку "Circle" или "Rectangle"? // console.log(shape.constructor.name); // Это ненадёжно } const shapes: Shape[] = [new Circle(10), new Rectangle(5, 7)]; shapes.forEach(printShapeType); ``` Я слышал, что есть какие-то декораторы или конструкция `instanceof`, но `instanceof` возвращает boolean, а не строку с именем. Подскажите, как правильно и надёжно получить runtime-имя класса в TypeScript? Важно, чтобы решение работало и после компиляции, и не ломалось при использовании Webpack или Terser для минификации кода. Заранее спасибо! <<>>

Проблема с 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.