Menu

Exclude, Extract и NonNullable в TypeScript: объяснение

Exclude, Extract и NonNullable фильтруют члены типа объединения. Что каждый оставляет и убирает, как выбирать члены объединения по структуре, как они построены из условных типов и чем отличаются от Omit и Pick.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Exclude<T, U> удаляет члены из типа объединения, Extract<T, U> оставляет только подходящие члены, а NonNullable<T> удаляет null и undefined. Все три фильтруют объединения; свойства объектов они не трогают.

Как и любые служебные типы, они существуют только при компиляции. Последнюю строку отвергает компилятор; выполняющийся JavaScript понятия не имеет, что такое Visible.

Exclude, Extract и NonNullable коротко

ТипОставляетПримерРезультат
Exclude<T, U>члены T, не присваиваемые в UExclude<"a" | "b" | "c", "a">"b" | "c"
Extract<T, U>члены T, присваиваемые в UExtract<"a" | "b" | "c", "a" | "z">"a"
NonNullable<T>всё, кроме null и undefinedNonNullable<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.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ