ПрограммированиеFullstack разработчик

Как работает Exclude в TypeScript, когда его использовать для манипуляций с union-типами, и какие бывают нюансы при работе с этим утилитарным типом?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

Exclude<T, U> — это utility type, появившийся в TypeScript для вычитания одного типа из другого, когда требуется исключить некоторые значения из union-типа.

История вопроса

Изначально в TypeScript не было удобного способа вычесть тип из другого. При создании обобщённых API, а также при рефакторинге часто требовалось получать "остаточный" тип — всё, кроме запрещённых значений. Вместо ручных манипуляций c union приходилось поддерживать несколько похожих интерфейсов.

Проблема

Например, когда есть тип 'A | B | C', но нужно получить тип без B. Это часто требуется при построении сложных входных параметров функций, фильтрации разрешённых значений и динамическом формировании типов.

Решение

Exclude решает данную проблему. Его упрощённая сигнатура такова:

type Exclude<T, U> = T extends U ? never : T;

Он возвращает тип, исключающий из T все члены U.

Пример:

type Status = 'draft' | 'published' | 'removed'; type UserVisibleStatus = Exclude<Status, 'removed'>; const visible: UserVisibleStatus = 'draft'; // OK

Ключевые особенности:

  • Позволяет формировать динамические типы за счёт "вычитания" частей из union.
  • Упрощает Refactoring — при изменении базового типа все производные автоматически обновляются.
  • Может быть использован, например, для фильтрации кейсов switch или ключей объекта.

Вопросы с подвохом.

Можно ли использовать Exclude для обычных типов, а не union?

Если T не является union-типа, но входит в U — Exclude все равно сработает, но результатом может быть never или T, что не всегда интуитивно.

Exclude<'a', 'a'> // результат: never Exclude<'a', 'b'> // результат: 'a'

Удаляет ли Exclude все упоминания типа в структуре объекта?

Нет, Exclude не проходит рекурсивно по вложенным полям типа, исключает только на верхнем уровне union.

Как работает Exclude с интерфейсами и типами-объектами?

Он сравнивает весь тип, а не отдельные свойства. Поэтому Exclude из union нескольких интерфейсов — удаляет только те, которые полностью совпадают с U.

interface A { x: number }; interface B { y: string }; // Exclude<A|B, B> даёт: A (B полностью совпадает)

Типовые ошибки и анти-паттерны

  • Попытки применять Exclude для вложенных или частичных совпадений
  • Использование для "удаления" свойств интерфейса, а не вариантов union
  • Игнорирование возможности получить тип never при полном совпадении типов

Пример из жизни

Негативный кейс

Валидация ролей пользователя через Exclude<UserRoles, 'admin'>, но забыли, что Exclude не применяется к вложенным структурам — права 'admin:sub' не были исключены.

Плюсы:

  • Простота формирования типа ролей.

Минусы:

  • Неочевидное поведение с вложенными или похожими типами; пропущена критичная роль.

Позитивный кейс

Использование Exclude для ограничения public API действиями: Exclude<Action, 'delete'>, что исключает опасную операцию.

Плюсы:

  • Безопасность на уровне типизации, нельзя вызвать запрещённое действие.

Минусы:

  • Нужно поддерживать актуальные списки base-типов.