Skip to main content

Утилітний тип exclude у TypeScript

Exclude<T, U> - утилітний тип TypeScript, який видаляє з union-типу всі члени, яким можна присвоїти U.

Теорія

TL;DR

  • Exclude<T, U> фільтрує union, прибираючи члени що збігаються з U
  • T - повний набір варіантів, U - ті, що треба прибрати
  • Протилежний до Extract, який залишає тільки збіги
  • Під капотом: 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 коротший для цього)
  • Звузити тип відповіді API перед передачею в обробник
  • Відфільтрувати string literal union: назви подій, action-типи, коди статусів
  • Створити підмножину великого union ролей або статусів для конкретного контексту

Типові помилки

Плутанина Exclude з Omit. Звучать схоже, але працюють з різними речами. 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 - це просто коротке скорочення для найпоширенішого випадку.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?