Утилітарний тип extract у TypeScript
Extract<T, U> бере з union-типу T лише ті члени, які можна присвоїти типу U, і повертає новий вужчий union із тих, що пройшли фільтр.
Теорія
TL;DR
- Уяви конвеєр на заводі: кидаєш ящик із деталями (union T), задаєш форму фільтра (U), на виході тільки ті, що підходять.
Extractперевіряє сумісність присвоєння (assignability), не точну рівність.- Якщо жоден член не пройшов фільтр, результат -
never. Це треба враховувати в сигнатурах функцій. - Використовуй, коли union надто широкий для конкретної гілки і потрібен точний підтип.
- Протилежність -
Exclude<T, U>.
Швидкий приклад
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. Очікування часткового збігу рядків
type Status = "error" | "ERR_404";
type Want = Extract<Status, "error">;
// Тільки "error". "ERR_404" не присвоюється літералу "error".Це різні рядкові літерали. Між літералами перевіряється точна відповідність. Якщо потрібні обидва, пиши Extract<Status, "error" | "ERR_404">.
2. Невідповідність вкладеної структури
type Deep = { a: "x" } | { a: { b: "y" } };
type Fail = Extract<Deep, { a: string }>;
// never! { b: "y" } не присвоюється stringTypeScript перевіряє всю структуру цілком. Вкладений об'єкт - не рядок. Якщо треба заглибитись у властивість, використовуй Extract<Deep["a"], string>.
3. Ігнорування результату never
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
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
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 не може перевірити самостійно.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.