Avatar ·

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

Всем привет! Я новичок в TypeScript и столкнулся с проблемой. У меня есть иерархия классов, и мне нужно в рантайме узнать, экземпляром какого именно класса является тот или иной объект. В обычном JavaScript я бы использовал `obj.constructor.name`, но в TypeScript это почему-то не работает так, как я ожидаю. Вот пример кода: ```typescript class Animal { name: string; constructor(name: string) { this.name = name; } } class Dog extends Animal { breed: string; constructor(name: string, breed: string) { super(name); this.breed = breed; } } const myPet = new Dog('Rex', 'Labrador'); // Пытаюсь получить имя класса console.log(myPet.constructor.name); // В JS это работает, но в TS... ``` Проблема в том, что при компиляции TypeScript в JavaScript, особенно если используется минификация или сборка (например, webpack с production-режимом), `constructor.name` может вернуть не "Dog", а какую-то однобуквенную строку или вообще `undefined`. Я читал, что это связано с тем, что имена классов могут быть изменены при транспиляции. Как правильно и надёжно получить имя класса объекта в TypeScript, чтобы это работало и в development, и в production сборке? Есть ли какие-то встроенные механизмы или паттерны для этого? Мне нужно это для логирования и отладки, а также для сериализации/десериализации объектов. Заранее спасибо за помощь! <<>>

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

Этот подход решает не только задачу получения имени класса, но и даёт мощный механизм для сериализации/десериализации. Минус — нужно явно регистрировать каждый класс, но это окупается в больших проектах с сложной иерархией.