Кто ты, объект? Узнаём имя класса в TypeScript (runtime)

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Зачем вам может понадобиться имя класса в рантайме?

Представьте: вы пишете сложную систему на 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                

Блоги

Рекомендаций по книгам