Що таке дистрибутивна поведінка union-типів?
Що таке дистрибутивна поведінка union-типів
Дистрибутивність (distributive behavior) означає,
що коли TypeScript застосовує умовний тип (extends ? :) до union-типу,
він автоматично розкладає вираз по кожному елементу об'єднання
і потім об'єднує результати назад у union.
Приклад 1. Проста ілюстрація
type T = string | number;
type Box<T> = T extends any ? { value: T } : never;Що станеться:
- TypeScript візьме
string - потім
number - застосує умову до кожного
- і об'єднає результати назад
type Result = Box<string | number>;
// Розкладається як:
// Box<string> | Box<number>
// -> { value: string } | { value: number }Підсумок: Box<string | number> ≡ { value: string } | { value: number }.
Чому так відбувається
Тому що в умовних типах, коли зліва від extends стоїть параметр-тип (T),
а не конкретний тип на кшталт {} чи unknown,
TypeScript розкриває об'єднання і застосовує умову окремо до кожного елемента.
Це особливе правило - "distributive conditional types".
Приклад 2. Перевіримо недистрибутивну поведінку
Якщо обгорнути T у "контейнер", дистрибутивність зникає:
type Boxed<T> = [T] extends [any] ? { value: T } : never;
type Result = Boxed<string | number>;
// Тепер не розподіляється!
// [string | number] не розширює [any] дистрибутивно
// Результат: { value: string | number }Різниця:
| Вираз | Результат |
|---|---|
T extends any ? X<T> : never | X<string> | X<number> |
[T] extends [any] ? X<T> : never | X<string | number> |
Обгортка [] вимикає дистрибутивність.
Приклад 3. Реальний кейс - вилучення підтипу
type ExtractString<T> = T extends string ? T : never;
type R = ExtractString<string | number | boolean>;
// розподіляється:
// string -> string
// number -> never
// boolean -> never
// об'єднуємо: string | never | never -> stringПідсумок:
ExtractString<string | number | boolean> -> string.
Так працюють вбудовані утиліти TypeScript на кшталт
Extract<>,Exclude<>,NonNullable<>та ін.
Приклад 4. Використання в Exclude і Extract
type Exclude<T, U> = T extends U ? never : T;
type R = Exclude<"a" | "b" | "c", "a" | "c">;
// розподіляється:
// "a" extends "a"|"c" -> never
// "b" extends "a"|"c" -> "b"
// "c" extends "a"|"c" -> never
// => "b"Ось чому
Exclude<"a" | "b" | "c", "a" | "c">дає"b".
Приклад 5. Порівняння з "недистрибутивною" версією
type NonDistributiveExclude<T, U> =
[T] extends [U] ? never : T;
type R = NonDistributiveExclude<"a" | "b" | "c", "a" | "c">;
// [T] не розкладається
// => ["a" | "b" | "c"] extends ["a" | "c"] -> false
// => результат = "a" | "b" | "c"Обгортаючи T у кортеж [],
ми вимикаємо дистрибутивність і змушуємо TypeScript розглядати об'єднання як одне ціле.
Приклад 6. Ще більш наочна візуалізація
Уяви union як множину:
T = {A, B, C}
T extends X ? Y : ZTypeScript робить:
(A extends X ? Y : Z)
| (B extends X ? Y : Z)
| (C extends X ? Y : Z)Ключові висновки
| Особливість | Поведінка |
|---|---|
Працює лише в умовних типах (extends ? :) | Так |
Активується, якщо T - naked type parameter (голий параметр) | Так |
Можна вимкнути, обгорнувши T у [] | Так |
Основа утиліт Exclude, Extract, NonNullable | Так |
| Застосовується поелементно до кожного члена union | Так |
Простий підсумок
Дистрибутивна поведінка union-типів - це коли TypeScript застосовує умовний тип до кожного елемента об'єднання окремо, а потім об'єднує результати назад у union.
Можна вимкнути це, обгорнувши тип у кортеж:
[T] extends [U] ? ... : ....
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.