Skip to main content

Що таке дистрибутивна поведінка union-типів?

Що таке дистрибутивна поведінка union-типів

Дистрибутивність (distributive behavior) означає, що коли TypeScript застосовує умовний тип (extends ? :) до union-типу, він автоматично розкладає вираз по кожному елементу об'єднання і потім об'єднує результати назад у union.


Приклад 1. Проста ілюстрація

javascript
type T = string | number; type Box<T> = T extends any ? { value: T } : never;

Що станеться:

  • TypeScript візьме string
  • потім number
  • застосує умову до кожного
  • і об'єднає результати назад
javascript
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 у "контейнер", дистрибутивність зникає:

javascript
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> : neverX<string> | X<number>
[T] extends [any] ? X<T> : neverX<string | number>

Обгортка [] вимикає дистрибутивність.


Приклад 3. Реальний кейс - вилучення підтипу

javascript
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

javascript
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. Порівняння з "недистрибутивною" версією

javascript
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 як множину:

javascript
T = {A, B, C} T extends X ? Y : Z

TypeScript робить:

javascript
(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] ? ... : ....

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

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

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