Как исключить класс из типа в TypeScript

Короткий ответ: если у вас есть union-тип, исключить из него определенный класс можно через utility type Exclude. Например: type WithoutAdmin = Exclude<UserUnion, AdminUser>. Но если нужно отфильтровать реальные объекты во время выполнения программы, одного типа мало: нужен instanceof, поле-дискриминатор или другой runtime-признак

Пример:

class AdminUser {
  constructor(public email: string, public permissions: string[]) {}
}

class ClientUser {
  constructor(public email: string, public orders: number[]) {}
}

type AnyUser = AdminUser | ClientUser;
type NonAdminUser = Exclude<AnyUser, AdminUser>;

NonAdminUser будет равен ClientUser

Exclude работает с типами, не с массивом

Exclude не удаляет объекты из массива. Он меняет тип на этапе компиляции

Если есть массив:

const users: AnyUser[] = [
  new AdminUser("admin@example.com", ["edit"]),
  new ClientUser("client@example.com", [1, 2]),
];

Чтобы получить только клиентов, нужна runtime-фильтрация:

const clients = users.filter(
  (user): user is NonAdminUser => !(user instanceof AdminUser)
);

Теперь clients имеет тип NonAdminUser[], и TypeScript знает, что там нет AdminUser

Вариант через discriminated union

С классами instanceof работает, но во многих проектах проще использовать объекты с полем type:

type Admin = {
  type: "admin";
  email: string;
  permissions: string[];
};

type Client = {
  type: "client";
  email: string;
  orders: number[];
};

type User = Admin | Client;
type NotAdmin = Exclude<User, { type: "admin" }>;

Фильтр:

const notAdmins = users.filter(
  (user): user is NotAdmin => user.type !== "admin"
);

Такой подход часто удобнее для данных из API, потому что JSON не сохраняет классы и прототипы

Когда Exclude не сработает как ожидается

Если тип не является union, исключать нечего:

type User = {
  email: string;
};

type Result = Exclude<User, { email: string }>;

Здесь можно получить never, потому что весь тип подходит под исключение. Exclude полезнее, когда есть варианты

Исключение по базовому классу

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

class SystemEvent {
  constructor(public createdAt: Date) {}
}

class UserEvent {
  constructor(public userId: number) {}
}

class PaymentEvent extends SystemEvent {
  constructor(public amount: number) {
    super(new Date());
  }
}

type EventUnion = SystemEvent | UserEvent | PaymentEvent;
type UserOnlyEvent = Exclude<EventUnion, SystemEvent>;

Здесь важно понимать структурную типизацию TypeScript. Класс в TypeScript проверяется не только по имени, а по форме. Если два класса имеют одинаковые публичные поля, результат может отличаться от ожиданий. Для надежного разделения вариантов лучше добавлять поле-дискриминатор, например kind: "system" | "user" | "payment"

Частые ошибки

Первая ошибка — ожидать, что Exclude удалит элементы из массива. Нет, он работает только на уровне типов

Вторая ошибка — использовать instanceof для объектов, пришедших из JSON. После JSON.parse это уже обычные объекты, не экземпляры класса

Третья ошибка — забыть type predicate в filter. Без (user): user is NonAdminUser TypeScript может не сузить тип достаточно точно

Четвертая ошибка — использовать any. Если исходный тип any, Exclude почти не помогает

Пятая ошибка — забывать про структурную природу TypeScript. Исключение класса не всегда работает как номинальная система типов из C# или Java, если классы похожи по публичным полям

Самопроверка

Создайте два класса, union из них и тип через Exclude. Затем отфильтруйте массив через instanceof и попробуйте обратиться к полю, которое есть только у исключенного класса. TypeScript должен запретить такой доступ

Что почитать дальше по TypeScript

Если нужен общий маршрут по теме, откройте рубрику TypeScript. Для соседних задач пригодятся эти разборы:

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x