Утилітний тип exclude у TypeScript
Exclude<T, U> - утилітний тип TypeScript, який видаляє з union-типу всі члени, яким можна присвоїти U.
Теорія
TL;DR
Exclude<T, U>фільтрує union, прибираючи члени що збігаються зUT- повний набір варіантів,U- ті, що треба прибрати- Протилежний до
Extract, який залишає тільки збіги - Під капотом:
T extends U ? never : T - Якщо всі члени
Tзбігаються зU, результат -never
Швидкий приклад
type Status = 'active' | 'inactive' | 'banned' | 'deleted';
// Прибрати статуси, не потрібні в цьому контексті
type VisibleStatus = Exclude<Status, 'banned' | 'deleted'>;
// Результат: 'active' | 'inactive'Два члени зникли. Решта залишилась.
Як компілятор це обробляє
Exclude визначений у стандартній бібліотеці TypeScript як distributive conditional type (розподільний умовний тип):
type Exclude<T, U> = T extends U ? never : T;TypeScript перевіряє кожен член union окремо. Для Exclude<'a' | 'b' | 'c', 'a'>:
'a' extends 'a'→never'b' extends 'a'→'b''c' extends 'a'→'c'
Об'єднуємо результати: never | 'b' | 'c', що спрощується до 'b' | 'c'. TypeScript прибирає never з union автоматично.
Exclude проти Extract
Ці два типи - дзеркала один одного.
| Утиліта | Що робить |
|---|---|
Exclude<T, U> | Видаляє з T всі члени, сумісні з U |
Extract<T, U> | Залишає в T тільки члени, сумісні з U |
type Mixed = string | number | boolean;
type OnlyStrings = Exclude<Mixed, number | boolean>; // string
type NumbersAndBools = Extract<Mixed, number | boolean>; // number | booleanОднакові вхідні дані, протилежні результати.
Коли використовувати
- Прибрати
nullіundefinedз union перед роботою зі значеннями (хочаNonNullableкоротший для цього) - Звузити тип відповіді API перед передачею в обробник
- Відфільтрувати string literal union: назви подій, action-типи, коди статусів
- Створити підмножину великого union ролей або статусів для конкретного контексту
Типові помилки
Плутанина Exclude з Omit. Звучать схоже, але працюють з різними речами. Omit видаляє ключі з типу об'єкта. Exclude видаляє члени з union.
// Неправильно: не прибере властивість з об'єкта
type WithoutKind = Exclude<{ kind: 'circle'; size: number }, 'kind'>;
// Все одно { kind: 'circle'; size: number } - нічого не змінилось
// Правильно: для ключів об'єкта є Omit
type WithoutKind2 = Omit<{ kind: 'circle'; size: number }, 'kind'>;
// { size: number }Помилка в рядковому літералі. Якщо значення в U не збігається з жодним членом T, нічого не видаляється і TypeScript не дає помилки.
type Roles = 'admin' | 'editor' | 'viewer';
// Помилка в назві: 'editer' нічому не відповідає
type Result = Exclude<Roles, 'editer'>; // 'admin' | 'editor' | 'viewer' - без змінЖодного попередження, жодної помилки. Це може з'їсти пів години дебагінгу. Завжди перевіряй орфографію в U.
Очікування фільтрації за полями об'єкта. Exclude розподіляє перевірку по членах union, а не по полях всередині одного об'єкта.
type User = { role: 'admin' | 'viewer' };
// Не відфільтрує по .role - перевіряється весь об'єкт User
type AdminOnly = Exclude<User, { role: 'viewer' }>; // Все одно UserЯкщо потрібно фільтрувати об'єкти, кожен варіант має бути окремим членом union, а не властивістю всередині одного типу.
Де зустрічається в реальному коді
- Redux/Zustand: виключити
'INIT'або'RESET'з union дій при описі конкретних обробників - React props: звузити string literal prop у дочірньому компоненті без повного перевизначення типу
- Стан форми:
Exclude<FieldStatus, 'untouched'>для типізації тільки полів, з якими взаємодіяв користувач - API-клієнт: прибрати
null | undefinedз union відповіді перед передачею даних у парсер
Питання на співбесіді
Q: Яка внутрішня реалізація Exclude у TypeScript?
A: type Exclude<T, U> = T extends U ? never : T. Це distributive conditional type, тому TypeScript застосовує перевірку до кожного члена union окремо, а не до всього union цілком.
Q: Яка різниця між Exclude і Omit?
A: Exclude працює з членами union, Omit - з ключами об'єкта. Exclude<'a' | 'b', 'a'> дає 'b'. Omit<{ a: 1; b: 2 }, 'a'> дає { b: 2 }.
Q: Що поверне Exclude<string, string>?
A: never. Кожен член T сумісний з U, тому всі стають never. Порожній union у TypeScript - це never.
Q: Що станеться з розподілом, якщо T загорнутий у кортеж?
A: Розподіл зупиниться. [T] extends [U] перевіряє кортеж цілком, а не кожен член union окремо. Це поширений edge case при написанні складних generic-утиліт, де розподіл навмисно пригнічується.
Приклади
Фільтрація HTTP-методів
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';
// Залишити тільки методи для читання
type SafeMethod = Exclude<HttpMethod, 'POST' | 'PUT' | 'DELETE' | 'PATCH'>;
// 'GET' | 'OPTIONS'
function readOnly(method: SafeMethod, url: string) {
return fetch(url, { method });
}
readOnly('GET', '/api/users'); // ok
readOnly('POST', '/api/users'); // TS error: 'POST' не входить у SafeMethodОписуєш повний набір один раз, а потім вирізаєш підмножини де потрібно. Без дублювання.
Видалення null з типу відповіді API
type ApiStatus = 'ok' | 'error' | 'pending' | null | undefined;
// Після отримання відповіді - null і undefined вже неактуальні
type ResolvedStatus = Exclude<ApiStatus, null | undefined>;
// 'ok' | 'error' | 'pending'
function handleResolved(status: ResolvedStatus) {
if (status === 'ok') {
console.log('Запит успішний');
}
}NonNullable<ApiStatus> дає той самий результат. Обидва варіанти коректні. NonNullable - це просто коротке скорочення для найпоширенішого випадку.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.