Skip to main content

Утилітарний тип extract у TypeScript

Extract<T, U> бере з union-типу T лише ті члени, які можна присвоїти типу U, і повертає новий вужчий union із тих, що пройшли фільтр.

Теорія

TL;DR

  • Уяви конвеєр на заводі: кидаєш ящик із деталями (union T), задаєш форму фільтра (U), на виході тільки ті, що підходять.
  • Extract перевіряє сумісність присвоєння (assignability), не точну рівність.
  • Якщо жоден член не пройшов фільтр, результат - never. Це треба враховувати в сигнатурах функцій.
  • Використовуй, коли union надто широкий для конкретної гілки і потрібен точний підтип.
  • Протилежність - Exclude<T, U>.

Швидкий приклад

ts
type Status = "success" | "error" | "pending" | 200; type StringStatuses = Extract<Status, string>; // Результат: "success" | "error" | "pending" // 200 - число, не присвоюється string, відфільтровується const code: StringStatuses = "error"; // OK const num: StringStatuses = 200; // Помилка типу

Extract пройшовся по кожному члену Status і залишив тільки ті, що присвоюються string. Число 200 не пройшло.

Ключова різниця

Extract перевіряє сумісність присвоєння, а не точну рівність. Тому Extract<"success" | "error", string> поверне "success" | "error", бо обидва рядкові літерали присвоюються string. Те ж стосується об'єктів: якщо член union має всі властивості, які вимагає U, плюс додаткові, він однаково проходить фільтр.

Коли використовувати

  • Відповідь API занадто широка: витягни підтип помилки або успіху для окремого обробника.
  • Ролі та доступ: витягни адмінські ролі з union усіх ролей.
  • Discriminated union: витягни варіанти з конкретним значенням дискримінанта.
  • Фільтрація подій: залиш тільки ті типи подій, які твій обробник реально покриває.

Як компілятор обробляє Extract

TypeScript розкриває Extract<T, U> як T extends U ? T : never, застосовуючи умовний тип до кожного члена union окремо. Для кожного перевіряється структурна сумісність. Жодних витрат у рантаймі - тип стирається під час компіляції. Ця поведінка доступна з TypeScript 2.8, коли з'явились conditional types.

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

1. Очікування часткового збігу рядків

ts
type Status = "error" | "ERR_404"; type Want = Extract<Status, "error">; // Тільки "error". "ERR_404" не присвоюється літералу "error".

Це різні рядкові літерали. Між літералами перевіряється точна відповідність. Якщо потрібні обидва, пиши Extract<Status, "error" | "ERR_404">.

2. Невідповідність вкладеної структури

ts
type Deep = { a: "x" } | { a: { b: "y" } }; type Fail = Extract<Deep, { a: string }>; // never! { b: "y" } не присвоюється string

TypeScript перевіряє всю структуру цілком. Вкладений об'єкт - не рядок. Якщо треба заглибитись у властивість, використовуй Extract<Deep["a"], string>.

3. Ігнорування результату never

ts
type None = Extract<"a" | "b", number>; // never function handle(x: None) {} // Функцію не можна викликати

Коли жоден член не проходить фільтр, отримуєш never. Змінну з типом never не можна нічим заповнити. Перевіряй тип фільтра перед тим, як використовувати результат у сигнатурах функцій.

Де зустрічається в реальному коді

У більшості React-проектів, з якими я стикався, Extract найчастіше зустрічається саме навколо типів відповіді API та перевірки ролей. Кілька конкретних прикладів:

  • React Query: Extract<UseQueryResult, { data: T }> розрізняє стани loading і success.
  • Zod: витягує конкретні валідатори зі схеми-union типу z.ZodTypeAny.
  • tRPC: фільтрує процедури роутера за формою вхідних даних.
  • Захист маршрутів: Extract<AppRole, "admin" | "superadmin"> для сторінок з обмеженим доступом.

Питання на співбесіді

Q: Що поверне Extract<'a' | 'b', string>?
A: 'a' | 'b'. Обидва рядкові літерали присвоюються string, тому обидва проходять без змін.

Q: Чим Extract<T, U> відрізняється від T & U?
A: & створює перетин (intersection) властивостей і може дати never при конфлікті примітивів. Extract вибирає цілі члени union, які відповідають U, зберігаючи їхню оригінальну форму.

Q: Розгорни Extract<'a' | 1, string | number> вручну.
A: ('a' extends string | number ? 'a' : never) | (1 extends string | number ? 1 : never) дорівнює 'a' | 1. Обидва члени проходять.

Q: Чому Extract<{x: 1}, {x: string}> дорівнює never?
A: Бо 1 не присвоюється string. Структурна перевірка не проходить по властивості x.

Q: (Senior) Як реалізувати inverse для Extract?
A: Вбудований варіант - Exclude<T, U>. Власна реалізація: T extends U ? never : T.

Приклади

Базовий: фільтрація mixed union

ts
type Event = "click" | "keydown" | "focus" | "drag"; type KeyboardEvents = Extract<Event, "keydown" | "focus">; // "keydown" | "focus" function handleKeyboard(event: KeyboardEvents) { // TypeScript знає, що event - тільки "keydown" або "focus" console.log("Keyboard event:", event); }

Два рядкові літерали проходять фільтр, решта видаляється з типу. Сигнатура функції тепер відображає лише те, що вона реально обробляє.

Середній: обробник відповіді API

ts
type ApiResponse = | { status: "success"; data: string } | { status: "error"; message: string } | null | undefined; type ErrorResponse = Extract<ApiResponse, { status: "error" }>; // { status: "error"; message: string } function handleError(response: ErrorResponse) { console.log("Error:", response.message); // TypeScript знає, що .message існує } function processResponse(response: ApiResponse) { if (response && response.status === "error") { handleError(response); // Присвоюється, бо guard звужує тип до ErrorResponse } }

Extract дає точний тип для гілки обробки помилки. Без нього довелось би писати type assertion або ручний type guard, який TypeScript не може перевірити самостійно.

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

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

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

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