Avatar ·

Как проверить тип класса в TypeScript: instanceof, type guards и защита типов

Всем привет! Я новичок в TypeScript и столкнулся с проблемой проверки типов классов. У меня есть иерархия классов: базовый класс `Animal` и два дочерних — `Dog` и `Cat`. У каждого из них есть свои уникальные методы: `bark()` у Dog и `meow()` у Cat. Я получаю массив объектов типа `Animal[]` из внешнего источника (например, API), и мне нужно определить, к какому именно классу относится каждый объект, чтобы вызвать соответствующий метод. Вот пример кода: ```typescript class Animal { name: string; constructor(name: string) { this.name = name; } } class Dog extends Animal { bark() { console.log(`${this.name} says: Woof!`); } } class Cat extends Animal { meow() { console.log(`${this.name} says: Meow!`); } } const animals: Animal[] = [new Dog('Rex'), new Cat('Molly')]; animals.forEach(animal => { // Как проверить, что animal — это Dog или Cat? // if (animal instanceof Dog) { ... } — это работает? // Или нужно использовать typeof? }); ``` Проблема в том, что `typeof` для экземпляров классов всегда возвращает `"object"`, а `instanceof` вроде работает, но я не уверен, что это правильный подход в TypeScript. Также я слышал про "type guards" и "user-defined type guards", но не понимаю, как их применять в этом случае. Подскажите, как правильно и безопасно с точки зрения типов проверять класс объекта в TypeScript? Особенно интересует, как избежать ошибок на этапе компиляции и сделать код идиоматичным. <<>>

Основная проблема: динамическая проверка типов в статически типизированном языке

TypeScript — это статическая типизация, которая работает во время компиляции. Но во время выполнения JavaScript не знает о типах TypeScript. Поэтому для проверки типа класса в рантайме мы используем стандартные механизмы JavaScript, но оборачиваем их в TypeScript-конструкции, чтобы компилятор понимал, что происходит.

Решение 1: Оператор instanceof (самый простой и правильный)

Оператор instanceof проверяет, присутствует ли прототип класса в цепочке прототипов объекта. В TypeScript он работает как type guard — после проверки TypeScript автоматически сужает тип:

animals.forEach(animal => {
  if (animal instanceof Dog) {
    // TypeScript знает, что здесь animal — это Dog
    animal.bark(); // ✅ OK
  } else if (animal instanceof Cat) {
    animal.meow(); // ✅ OK
  }
});

Важно: instanceof работает только с классами (и конструкторами функций). Он не работает с интерфейсами или типами — они стираются во время компиляции.

Решение 2: User-defined type guards (пользовательские защитники типов)

Если логика проверки сложнее или вы хотите её переиспользовать, создайте функцию-предикат с возвращаемым типом animal is Dog:

function isDog(animal: Animal): animal is Dog {
  return animal instanceof Dog;
}

function isCat(animal: Animal): animal is Cat {
  return animal instanceof Cat;
}

animals.forEach(animal => {
  if (isDog(animal)) {
    animal.bark();
  } else if (isCat(animal)) {
    animal.meow();
  }
});

Такая запись явно говорит TypeScript: "Если функция возвращает true, то переданный объект гарантированно имеет тип Dog". Это улучшает читаемость и позволяет использовать проверку в разных местах.

Решение 3: Дискриминирующие объединения (Discriminated Unions)

Если вы контролируете структуру классов, добавьте поле-дискриминатор (например, kind или type). Это самый надёжный способ, так как он не зависит от цепочки прототипов:

class Animal {
  readonly kind: string;
  name: string;
  constructor(kind: string, name: string) {
    this.kind = kind;
    this.name = name;
  }
}

class Dog extends Animal {
  readonly kind = 'dog';
  constructor(name: string) {
    super('dog', name);
  }
  bark() { ... }
}

class Cat extends Animal {
  readonly kind = 'cat';
  constructor(name: string) {
    super('cat', name);
  }
  meow() { ... }
}

// Type Guard с дискриминатором
function isDog(animal: Animal): animal is Dog {
  return animal.kind === 'dog';
}

Чего делать НЕ стоит

  • Не используйте typeof — для экземпляров классов он всегда возвращает "object".
  • Не проверяйте по названию конструктора (animal.constructor.name === 'Dog') — это ненадёжно при минификации кода.
  • Не используйте as (type assertion) без проверки — это подавит ошибки компиляции, но не защитит от ошибок в рантайме.

Итог: когда что использовать?

  • Если у вас простые классы и вы уверены в цепочке прототипов — используйте instanceof.
  • Если логика проверки сложная или переиспользуется — оберните в user-defined type guard.
  • Если вы проектируете систему с нуля — используйте дискриминирующие объединения (поле kind). Это самый типобезопасный и предсказуемый подход.
<<>>

Альтернативный подход: проверка по методам с помощью in оператора

Иногда instanceof не подходит — например, если объекты приходят из JSON (сериализованные) и не имеют прототипной связи с классами. В таких случаях можно проверить наличие специфических методов с помощью оператора in:

function isDog(animal: Animal): animal is Dog {
  return 'bark' in animal;
}

function isCat(animal: Animal): animal is Cat {
  return 'meow' in animal;
}

Плюсы:

  • Работает даже с простыми объектами (plain objects), если они имеют нужные методы.
  • Не требует наследования — подходит для duck typing.

Минусы:

  • Ненадёжно, если метод может быть унаследован от другого класса или добавлен через прототип.
  • Не защищает от коллизий имён (например, если у Cat тоже есть метод bark).

Когда использовать?

Этот подход хорош для работы с данными из внешних источников (API, localStorage), где объекты не являются экземплярами ваших классов, но имеют схожую структуру. В таком случае лучше комбинировать проверку in с проверкой типов полей.

interface DogLike {
  bark(): void;
  name: string;
}

function isDogLike(obj: any): obj is DogLike {
  return typeof obj.name === 'string' && typeof obj.bark === 'function';
}

Этот метод ближе к функциональному стилю и часто используется в связке с интерфейсами вместо классов.