Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Утилітний тип exclude у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`Exclude<T, U>`** видаляє з union-типу всі члени, яким можна присвоїти `U`. ```ts type Status = 'ok' | 'error' | null; type Clean = Exclude<Status, null>; // 'ok' | 'error' ``` **Ключове:** реалізований як `T extends U ? never : T`, TypeScript перевіряє кожен член union окремо і прибирає ті, що збігаються.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`Exclude<T, U>`** - утилітний тип TypeScript, який видаляє з union-типу всі члени, яким можна присвоїти `U`. ## Теорія ### TL;DR - `Exclude<T, U>` фільтрує union, прибираючи члени що збігаються з `U` - `T` - повний набір варіантів, `U` - ті, що треба прибрати - Протилежний до [`Extract`](/questions/utility-type-extract-in-typescript), який залишає тільки збіги - Під капотом: `T extends U ? never : T` - Якщо всі члени `T` збігаються з `U`, результат - `never` ### Швидкий приклад ```ts type Status = 'active' | 'inactive' | 'banned' | 'deleted'; // Прибрати статуси, не потрібні в цьому контексті type VisibleStatus = Exclude<Status, 'banned' | 'deleted'>; // Результат: 'active' | 'inactive' ``` Два члени зникли. Решта залишилась. ### Як компілятор це обробляє `Exclude` визначений у стандартній бібліотеці TypeScript як distributive conditional type (розподільний умовний тип): ```ts type Exclude<T, U> = T extends U ? never : T; ``` TypeScript перевіряє кожен член union окремо. Для `Exclude<'a' | 'b' | 'c', 'a'>`: 1. `'a' extends 'a'` → `never` 2. `'b' extends 'a'` → `'b'` 3. `'c' extends 'a'` → `'c'` Об'єднуємо результати: `never | 'b' | 'c'`, що спрощується до `'b' | 'c'`. TypeScript прибирає `never` з union автоматично. ### Exclude проти Extract Ці два типи - дзеркала один одного. | Утиліта | Що робить | |---|---| | `Exclude<T, U>` | Видаляє з `T` всі члени, сумісні з `U` | | `Extract<T, U>` | Залишає в `T` тільки члени, сумісні з `U` | ```ts type Mixed = string | number | boolean; type OnlyStrings = Exclude<Mixed, number | boolean>; // string type NumbersAndBools = Extract<Mixed, number | boolean>; // number | boolean ``` Однакові вхідні дані, протилежні результати. ### Коли використовувати - Прибрати `null` і `undefined` з union перед роботою зі значеннями (хоча [`NonNullable`](/questions/utility-type-nonnullable-in-typescript) коротший для цього) - Звузити тип відповіді API перед передачею в обробник - Відфільтрувати string literal union: назви подій, action-типи, коди статусів - Створити підмножину великого union ролей або статусів для конкретного контексту ### Типові помилки **Плутанина `Exclude` з [`Omit`](/questions/utility-type-omit-in-typescript).** Звучать схоже, але працюють з різними речами. `Omit` видаляє ключі з типу об'єкта. `Exclude` видаляє члени з union. ```ts // Неправильно: не прибере властивість з об'єкта 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 не дає помилки. ```ts type Roles = 'admin' | 'editor' | 'viewer'; // Помилка в назві: 'editer' нічому не відповідає type Result = Exclude<Roles, 'editer'>; // 'admin' | 'editor' | 'viewer' - без змін ``` Жодного попередження, жодної помилки. Це може з'їсти пів години дебагінгу. Завжди перевіряй орфографію в `U`. **Очікування фільтрації за полями об'єкта.** `Exclude` розподіляє перевірку по членах union, а не по полях всередині одного об'єкта. ```ts 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-методів ```ts 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 ```ts 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` - це просто коротке скорочення для найпоширенішого випадку.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.