Avatar ·

Проверка типа класса в TypeScript: как определить, является ли объект экземпляром определённого класса?

Всем привет! Я недавно начал изучать TypeScript и столкнулся с проблемой при проверке типов во время выполнения. У меня есть иерархия классов: ```typescript class Animal { name: string; constructor(name: string) { this.name = name; } } class Dog extends Animal { bark() { console.log("Woof!"); } } class Cat extends Animal { meow() { console.log("Meow!"); } } ``` Я пишу функцию, которая принимает массив `Animal[]` и хочет выполнить разные действия для собак и кошек. В JavaScript я бы использовал `instanceof`, но TypeScript выдаёт ошибку, когда я пытаюсь сделать так: ```typescript function processAnimals(animals: Animal[]) { for (const animal of animals) { if (animal instanceof Dog) { // Ошибка: 'Dog' refers to a value, but is being used as a type here animal.bark(); } else if (animal instanceof Cat) { animal.meow(); } } } ``` TypeScript говорит: `'Dog' refers to a value, but is being used as a type here`. Я в замешательстве — ведь `Dog` — это и класс (значение), и тип. Как правильно проверить, является ли объект экземпляром определённого класса в TypeScript? Нужны ли какие-то специальные конструкции вроде type guards или пользовательских функций? Или я что-то неправильно импортирую/объявляю? Буду благодарен за примеры кода и объяснение, как работает `instanceof` в TypeScript. Спасибо! <<>>

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

Ошибка, которую вы получаете, возникает не из-за самого instanceof, а из-за того, как TypeScript обрабатывает импорты и объявления. Когда вы пишете if (animal instanceof Dog), TypeScript ожидает, что Dog — это значение (конструктор класса). Ошибка 'Dog' refers to a value, but is being used as a type here появляется, если Dog был импортирован или объявлен только как тип (например, import type { Dog } from './animals'), а не как класс. В вашем случае, если вы объявили классы в том же файле, instanceof должен работать. Давайте разберём правильный подход.

Решение: правильное использование instanceof

В TypeScript instanceof работает точно так же, как в JavaScript, при условии, что класс определён как значение (через class), а не как тип (type или interface). Вот исправленный код:

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

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

class Cat extends Animal {
  meow() {
    console.log("Meow!");
  }
}

function processAnimals(animals: Animal[]) {
  for (const animal of animals) {
    if (animal instanceof Dog) {
      // TypeScript теперь знает, что animal — это Dog
      animal.bark(); // ✅ OK
    } else if (animal instanceof Cat) {
      animal.meow(); // ✅ OK
    } else {
      console.log(`Just an animal: ${animal.name}`);
    }
  }
}

// Использование
const animals: Animal[] = [new Dog("Rex"), new Cat("Whiskers"), new Animal("Generic")];
processAnimals(animals);
// Вывод:
// Woof!
// Meow!
// Just an animal: Generic

Почему это работает?

instanceof является пользовательским защитником типа (type guard) в TypeScript. После проверки animal instanceof Dog TypeScript сужает тип animal с Animal до Dog внутри блока if. Это позволяет вызывать методы, специфичные для подкласса (bark(), meow()), без ошибок компиляции.

Если классы находятся в разных модулях

Если вы импортируете классы из другого файла, убедитесь, что используете обычный импорт, а не импорт типа:

// animals.ts
export class Dog extends Animal { /* ... */ }

// main.ts
import { Dog } from './animals'; // ✅ Правильно: импорт значения
// import type { Dog } from './animals'; // ❌ Ошибка: импорт только типа

if (animal instanceof Dog) { /* ... */ }

Дополнительные советы

  • Порядок проверок важен: Если у вас есть подклассы, проверяйте сначала самые специфичные (например, Dog и Cat), потом базовый класс (Animal).
  • Пользовательские type guards: Для более сложной логики можно создать свою функцию-защитник:
    function isDog(animal: Animal): animal is Dog {
      return animal instanceof Dog;
    }
    
    if (isDog(animal)) {
      animal.bark();
    }
  • Дискриминирующие объединения: Если у вас есть контроль над классами, можно добавить поле-дискриминатор:
    class Dog extends Animal {
      readonly kind = 'dog' as const;
      bark() { /* ... */ }
    }
    
    class Cat extends Animal {
      readonly kind = 'cat' as const;
      meow() { /* ... */ }
    }
    
    if (animal.kind === 'dog') {
      animal.bark(); // TypeScript сужает тип
    }

Заключение

instanceof — это полностью рабочий и правильный способ проверки типа класса в TypeScript, если класс определён как значение. Убедитесь, что вы не используете import type для классов, и всё будет работать. TypeScript автоматически сужает тип после проверки, что даёт вам безопасный доступ к методам подкласса.

<<>>

Альтернативный подход: проверка через конструктор

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

function processAnimals(animals: Animal[]) {
  for (const animal of animals) {
    if (animal.constructor === Dog) {
      // Здесь TypeScript не сузит тип автоматически!
      // Придётся использовать приведение:
      (animal as Dog).bark();
    } else if (animal.constructor === Cat) {
      (animal as Cat).meow();
    }
  }
}

Недостатки этого подхода:

  • TypeScript не может автоматически сузить тип после проверки constructor, поэтому приходится использовать as (приведение типа), что менее безопасно.
  • Не работает с наследованием: если Dog наследуется от Animal, animal.constructor === Dog вернёт true только для прямых экземпляров Dog, а не для подклассов Dog.
  • Может сломаться, если прототип класса был изменён.

Когда использовать: Только если instanceof недоступен из-за кросс-контекстных проблем. В обычной разработке на TypeScript всегда предпочитайте instanceof — он безопаснее, читаемее и даёт правильное сужение типов.