Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Утилітарний тип extract у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Extract<T, U>** залишає з union-типу T тільки ті члени, які присвоюються U. ```ts type Status = "success" | "error" | 200; type StringStatuses = Extract<Status, string>; // "success" | "error" ``` **Ключове:** Працює як `T extends U ? T : never`, застосований до кожного члена union окремо. Протилежність `Exclude`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 не може перевірити самостійно.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.