Exclude<T, U> удаляет члены из типа объединения, Extract<T, U> оставляет только подходящие члены, а NonNullable<T> удаляет null и undefined. Все три фильтруют объединения; свойства объектов они не трогают.
Как и любые служебные типы, они существуют только при компиляции. Последнюю строку отвергает компилятор; выполняющийся JavaScript понятия не имеет, что такое Visible.
Exclude, Extract и NonNullable коротко
| Тип | Оставляет | Пример | Результат |
|---|---|---|---|
Exclude<T, U> | члены T, не присваиваемые в U | Exclude<"a" | "b" | "c", "a"> | "b" | "c" |
Extract<T, U> | члены T, присваиваемые в U | Extract<"a" | "b" | "c", "a" | "z"> | "a" |
NonNullable<T> | всё, кроме null и undefined | NonNullable<string | null | undefined> | string |
Ключевые слова здесь «присваиваемые в U». U не обязан быть списком точных членов; это может быть более широкий тип, под который подходят несколько членов:
Выбор членов объединения по структуре
Для размеченного объединения Extract с частичной структурой вытаскивает варианты, у которых она есть. Это обычный способ назвать один вариант объединения, которое писали не вы:
Структура { type: "click" } является надтипом только варианта click, поэтому Extract оставляет именно его. Структура, подходящая под несколько вариантов (скажем, { x: number }, если бы она была у двух), оставляет их все.
NonNullable
NonNullable<T> это версия проверки !== null && !== undefined для типов. Он появляется, когда тип выводится из чего-то необязательного:
User["manager"] это { name: string } | null | undefined (? добавляет undefined), а NonNullable убирает оба. Начиная с TypeScript 5.5 компилятор выводит этот предикат и сам, так что raw.filter((s) => s != null) уже имеет тип string[]; явный s is NonNullable<typeof s> делает намерение видимым и работает в более старых версиях. Его определение type NonNullable<T> = T & {}: пустой объектный тип {} принимает любое значение, кроме null и undefined, поэтому пересечение с ним убирает ровно их.
Как они построены
Exclude и Extract это однострочные условные типы в стандартной библиотеке:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
Когда T это объединение, условный тип применяется к каждому члену отдельно (распределяется), а члены never исчезают из результата. Поэтому Exclude<"a" | "b", "a"> превращается в ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b"), то есть в never | "b", то есть в "b". Распределение подробно разобрано на странице условные типы.
Exclude и Omit, Extract и Pick
Названия легко перепутать, потому что обе пары что-то «убирают» или «оставляют». Разница в том, с чем они работают:
| Работает с | Убирает или оставляет | Пример | |
|---|---|---|---|
Exclude<T, U> | объединением | члены объединения | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> | объединением | члены объединения | Extract<"a" | "b", "a"> → "a" |
Omit<T, K> | объектным типом | свойства | Omit<User, "password"> |
Pick<T, K> | объектным типом | свойства | Pick<User, "name"> |
Использовать Exclude для объектного типа это частая оплошность, и она проходит молча:
Account не присваивается в "password", поэтому Exclude оставляет его целиком. Сам Omit построен из Exclude, применённого к ключам: Omit<T, K> это Pick<T, Exclude<keyof T, K>>. Подробнее в разделе Omit.
Более строгий Exclude
Второй параметр Exclude принимает любой тип, поэтому опечатка ничего не удаляет и не вызывает ошибки. Ограничение заставляет компилятор проверять, что исключаемое действительно входит в объединение:
index.ts(7,37): error TS2344: Type '"drafft"' does not satisfy the constraint 'Status'.
Исправьте написание, и код компилируется, а Strict равен "published" | "archived". Тот же приём работает для Extract и для строгого Omit (K extends keyof T).
Часто задаваемые вопросы
Что делает Exclude в TypeScript?
Exclude<T, U> удаляет из объединения T каждый член, который присваивается в U. Exclude<"a" | "b" | "c", "a"> это "b" | "c". Чтобы удалить несколько членов, передайте объединение как U: Exclude<T, "a" | "b">.
Чем Exclude отличается от Extract?
Они противоположны. Exclude<T, U> оставляет члены T, которые не присваиваются в U; Extract<T, U> оставляет только те, что присваиваются. Для одних и тех же T и U два результата вместе составляют T.
Чем Exclude отличается от Omit?
Exclude работает с типом объединения и удаляет члены объединения. Omit работает с объектным типом и удаляет свойства. Exclude<User, "password"> ничего полезного не делает (возвращает User), а Omit<User, "password"> даёт объектный тип без этого свойства. Omit построен на Exclude: он исключает ключ из keyof T.
Что делает NonNullable в TypeScript?
NonNullable<T> удаляет null и undefined из типа: NonNullable<string | null | undefined> это string. Он определён как T & {}, поскольку {} принимает любое значение, кроме null и undefined.
Почему Exclude не сообщает об ошибке для несуществующего члена?
Его второй параметр не ограничен, поэтому принимается любой тип, и опечатка вроде Exclude<Status, "drafft"> молча ничего не удаляет. Определите более строгую версию, type StrictExclude<T, U extends T> = Exclude<T, U>, чтобы компилятор проверял, что U входит в T.