Как проверить тип класса в TypeScript: от новичка до профи
Вы когда-нибудь писали на TypeScript, получали объект неизвестного типа и думали: "А кто ты, собственно, такой?"? Этот вопрос мучает тысячи разработчиков — не зря тема class type check in TypeScript собрала сотни тысяч просмотров на Stack Overflow. И сегодня мы раз и навсегда разберемся, как правильно проверять типы классов в TypeScript.
В этой статье вы узнаете:
- Почему
typeofне работает для классов - Как использовать
instanceofкак швейцарский нож - Что такое type guards и как они спасают ваш код
- Как написать свою проверку типа с помощью
is - Реальные примеры из практики
Поехали!
Проблема: почему typeof здесь бессилен?
В JavaScript есть оператор typeof, который отлично работает с примитивами:
typeof "hello" // "string"
typeof 42 // "number"
typeof true // "boolean"
Но когда дело доходит до классов, typeof пасует:
class Dog {}
class Cat {}
const pet = new Dog();
typeof pet; // "object" — бесполезно!
typeof Dog; // "function" — тоже не то
Почему? Потому что классы в JavaScript — это просто синтаксический сахар над функциями-конструкторами. typeof видит только "объект" или "функцию", но не различает, какой именно это класс.
Здесь на сцену выходит class type check in TypeScript — целый арсенал инструментов, который поможет вам точно определить, с экземпляром какого класса вы имеете дело.
Метод 1: instanceof — ваш главный помощник
Самый прямой и понятный способ проверить тип класса — оператор instanceof. Он проверяет, есть ли класс в цепочке прототипов объекта:
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 handleAnimal(animal: Animal) {
if (animal instanceof Dog) {
// TypeScript знает, что animal — это Dog
animal.bark();
} else if (animal instanceof Cat) {
animal.meow();
} else {
console.log(`Unknown animal: ${animal.name}`);
}
}
const rex = new Dog("Rex");
handleAnimal(rex); // "Woof!"
Обратите внимание: после проверки animal instanceof Dog TypeScript автоматически сужает тип (type narrowing) и позволяет вызывать методы Dog без ошибок. Это и есть магия type guards — встроенных защитников типов.
Когда instanceof не работает?
- При работе с разными окнами/фреймами (разные глобальные объекты)
- Если классы определены в разных модулях с разными версиями
- При проверке интерфейсов (они не существуют в рантайме)
Метод 2: Собственные type guards с оператором is
Иногда instanceof недостаточно. Например, вы хотите проверить не только класс, но и дополнительные условия. Здесь на помощь приходят пользовательские type guards с ключевым словом is:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Bird | Fish) {
if (isFish(pet)) {
// TypeScript знает: pet — это Fish
pet.swim();
} else {
pet.fly();
}
}
Обратите внимание на синтаксис: pet is Fish в возвращаемом типе. Это говорит TypeScript: "Если эта функция возвращает true, то параметр pet — это Fish". Теперь TypeScript будет сужать тип везде, где вы используете isFish().
Проверка типа класса с дополнительной логикой
class User {
constructor(
public id: number,
public name: string,
public role: string
) {}
}
class Admin extends User {
constructor(id: number, name: string) {
super(id, name, "admin");
}
deleteUser(userId: number) {
console.log(`Deleting user ${userId}`);
}
}
function isAdmin(user: User): user is Admin {
return user instanceof Admin && user.role === "admin";
}
function processUser(user: User) {
if (isAdmin(user)) {
// Безопасно вызываем метод Admin
user.deleteUser(42);
} else {
console.log(`User ${user.name} has role: ${user.role}`);
}
}
Такой class type check in TypeScript дает вам полный контроль: вы проверяете не только наследование, но и бизнес-логику.
Метод 3: Дискриминирующие объединения (Discriminated Unions)
Это продвинутый паттерн, который часто используется вместо проверки классов. Идея в том, чтобы добавить в каждый класс/интерфейс уникальное свойство-дискриминатор:
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
TypeScript автоматически сужает тип в каждой ветке switch или if. Этот подход часто предпочтительнее instanceof, потому что:
- Работает с интерфейсами (нет рантайм-классов)
- Не зависит от прототипов
- Легче рефакторится
Метод 4: Проверка с помощью Symbol.hasInstance
Малоизвестный, но мощный способ — переопределить статическое свойство Symbol.hasInstance в классе. Это меняет поведение instanceof для вашего класса:
class SpecialNumber {
static [Symbol.hasInstance](value: unknown) {
return typeof value === "number" && value > 100;
}
}
console.log(50 instanceof SpecialNumber); // false
console.log(200 instanceof SpecialNumber); // true
console.log("hello" instanceof SpecialNumber); // false
Это экзотика, но иногда очень полезно, когда стандартная проверка типа класса не подходит.
Реальный пример: Система обработки событий
Давайте соберем все вместе в реальном сценарии. Представьте, что вы пишете библиотеку для обработки событий:
abstract class Event {
constructor(
public readonly timestamp: number = Date.now()
) {}
}
class ClickEvent extends Event {
constructor(
public x: number,
public y: number
) {
super();
}
}
class KeyPressEvent extends Event {
constructor(
public key: string,
public ctrlKey: boolean