Пошук уроків, статей та іншого контенту
Зрозумієте, як умовні типи розподіляються по union, і навчитеся керувати або вимикати таку поведінку.
Умовний тип має такий синтаксис:
T extends U ? TypeIfTrue : TypeIfFalseЯкщо умовний тип використовує параметр типу T безпосередньо зліва від extends, TypeScript розподіляє його по кожному елементу union.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;TypeScript обробляє string | number окремо:
type Result =
| ToArray<string>
| ToArray<number>
| string[]
| number[];Отже, Result дорівнює:
string[] | number[]Це називається дистрибутивністю умовного типу.
Розглянемо умовний тип, який перетворює кожен тип на масив:
type ToArray<T> = T extends unknown ? T[] : never;
type Values = ToArray<string | number>;Логічно це можна записати так:
type Values =
| (string extends unknown ? string[] : never)
| (number extends unknown ? number[] : never);Після обчислення умови:
type Values = string[] | number[];Тому такі значення є коректними:
const strings: Values = ["TypeScript", "JavaScript"];
const numbers: Values = [1, 2, 3];А масив зі змішаними значеннями — ні:
// Помилка: тип (string | number)[] не належить до string[] | number[]
const mixed: Values = ["TypeScript", 2024];Результат string[] | number[] відрізняється від (string | number)[].
string[] | number[] — або масив лише рядків, або масив лише чисел.
(string | number)[] — масив, у якому можуть бути і рядки, і числа.
Умовний тип є дистрибутивним, коли одночасно виконуються такі умови:
Використовується узагальнений параметр типу.
Цей параметр знаходиться безпосередньо зліва від extends.
Параметр отримує union як аргумент.
type IsString<T> = T extends string ? true : false;
type Result = IsString<string | number>;TypeScript розподілить union:
type Result =
| IsString<string>
| IsString<number>
| true
| false;Тому:
type Result = true | false;Це не те саме, що перевірити весь union як єдине значення.
neverДистрибутивні умовні типи зручно використовувати для фільтрування union.
Якщо гілка умовного типу повертає never, відповідний елемент зникає з union:
type OnlyStrings<T> = T extends string ? T : never;
type Result = OnlyStrings<string | number | boolean>;Розподіл відбувається так:
type Result =
| OnlyStrings<string>
| OnlyStrings<number>
| OnlyStrings<boolean>;Після обчислення:
type Result =
| string
| never
| never;Оскільки never не додає значень до union:
type Result = string;Цей підхід лежить в основі багатьох стандартних утиліт TypeScript, зокрема Extract і Exclude.
ExcludeТип Exclude<T, U> видаляє з T усі типи, сумісні з U. Спрощений аналог можна реалізувати так:
type Without<T, U> = T extends U ? never : T;
type Primitive = string | number | boolean;
type WithoutBoolean = Without<Primitive, boolean>;Результат:
type WithoutBoolean = string | number;Кожен елемент union перевіряється окремо:
string не є boolean, тому залишається;
number не є boolean, тому залишається;
boolean є boolean, тому замінюється на never.
Можна залишати лише ті варіанти, які відповідають певній формі:
type Event =
| { kind: "click"; x: number; y: number }
| { kind: "keypress"; key: string }
| { kind: "focus"; element: string };
type ByKind<T, K> = T extends { kind: K } ? T : never;
type ClickEvent = ByKind<Event, "click">;ClickEvent матиме тип:
type ClickEvent = {
kind: "click";
x: number;
y: number;
};TypeScript перевірив кожен варіант Event окремо й залишив лише той, де kind дорівнює "click".
Іноді потрібно перевірити весь union, а не кожен його елемент окремо.
Для цього параметр типу обгортають у кортеж:
type IsUnionAssignableToString<T> =
[T] extends [string] ? true : false;Тепер T не знаходиться безпосередньо зліва від extends. TypeScript сприймає весь тип як єдине значення.
type A = IsUnionAssignableToString<string | number>;
type B = IsUnionAssignableToString<string>;Результати:
type A = false;
type B = true;Без кортежів умова була б дистрибутивною:
type DistributiveCheck<T> =
T extends string ? true : false;
type Result = DistributiveCheck<string | number>;
// true | falseЗ кортежами перевіряється весь union:
type NonDistributiveCheck<T> =
[T] extends [string] ? true : false;
type Result = NonDistributiveCheck<string | number>;
// falseПорівняймо дистрибутивний і недистрибутивний варіанти:
type DistributiveArray<T> =
T extends unknown ? T[] : never;
type NonDistributiveArray<T> =
[T] extends [unknown] ? T[] : never;
type Distributed = DistributiveArray<string | number>;
// string[] | number[]
type NotDistributed = NonDistributiveArray<string | number>;
// (string | number)[]У першому випадку кожен елемент union перетворюється на окремий масив.
У другому випадку спочатку перевіряється весь string | number, після чого створюється один масив для всього union.
type ToArray<T> = T extends unknown ? T[] : never;
type ToSingleArray<T> = [T] extends [unknown] ? T[] : never;
type Distributed = ToArray<string | number>;
type NonDistributed = ToSingleArray<string | number>;
const strings: Distributed = ["TypeScript", "JavaScript"];
const numbers: Distributed = [1, 2, 3];
const mixed: NonDistributed = ["TypeScript", 2024, true];
function printValues(values: Distributed): void {
if (values.every((value) => typeof value === "string")) {
console.log("Рядки:", values.join(", "));
} else {
console.log("Числа:", values.join(", "));
}
}
printValues(strings);
printValues(numbers);У цьому прикладі:
Distributed дорівнює string[] | number[];
NonDistributed дорівнює (string | number)[];
strings і numbers можуть містити лише один тип елементів;
mixed може містити різні типи, оскільки дистрибутивність вимкнена.
neverДистрибутивні умовні типи мають особливу поведінку для never.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<never>;Оскільки never представляє union без жодного варіанта, розподіляти його немає по чому. Результатом також буде:
type Result = never;Це часто корисно під час фільтрування типів:
type OnlyNumbers<T> = T extends number ? T : never;
type Result = OnlyNumbers<string | boolean>;
// neverЖоден елемент початкового union не відповідає умові, тому всі вони перетворюються на never.
Якщо потрібно перевірити саме never, кортежі також допомагають уникнути розподілу:
type IsNever<T> = [T] extends [never] ? true : false;
type A = IsNever<never>;
// true
type B = IsNever<string>;
// falseБез кортежів перевірка T extends never могла б сама стати дистрибутивною і повернути неочікуваний результат.
Дистрибутивність залежить не від назви параметра, а від його позиції.
Цей тип є дистрибутивним:
type Distribute<T> = T extends string ? T[] : never;А цей — ні:
type DoNotDistribute<T> = Array<T> extends Array<string>
? T[]
: never;У другому прикладі зліва від extends знаходиться Array<T>, а не сам параметр T. Тому TypeScript не розподіляє union.
Так само дистрибутивність вимикають будь-які обгортки, які змінюють безпосередню форму параметра:
type WithoutDistribution<T> =
{ value: T } extends { value: string }
? T
: never;Найпоширеніший і найзрозуміліший спосіб явно вимкнути дистрибутивність — використати кортеж:
type WithoutDistribution<T> =
[T] extends [string] ? T : never;type IsString<T> = T extends string ? "yes" : "no";
type Result = IsString<string | number>;
// "yes" | "no"Іноді очікують "no", тому що весь string | number не є підтипом string. Але цей тип дистрибутивний і перевіряє string та number окремо.
Якщо потрібно перевірити весь union:
type IsEntirelyString<T> =
[T] extends [string] ? "yes" : "no";
type Result = IsEntirelyString<string | number>;
// "no"string[] | number[] і (string | number)[]Це різні типи:
type SeparateArrays = string[] | number[];
type MixedArray = (string | number)[];SeparateArrays дозволяє лише однорідні масиви, а MixedArray — змішані.
neverПід час фільтрування never зникає з union:
type OnlyStrings<T> = T extends string ? T : never;
type Result = OnlyStrings<string | number>;
// stringЦе не помилка, а основний механізм фільтрування union.
Дистрибутивність вимикається, коли сам параметр не стоїть безпосередньо зліва від extends:
type NonDistributive<T> =
[T] extends [string] ? true : false;Проте потрібно враховувати, яку саме перевірку ви хочете виконати. Обгортка змінює семантику: замість перевірки кожного елемента відбувається перевірка всього union.
Умовний тип виду T extends U ? A : B є дистрибутивним, якщо T — безпосередній параметр зліва від extends.
Для union TypeScript застосовує такий тип до кожного елемента окремо.
Дистрибутивність дає змогу фільтрувати union за допомогою never.
string | number після дистрибутивного перетворення на масив стає string[] | number[].
Щоб отримати (string | number)[], потрібно вимкнути розподіл.
Найпоширеніший спосіб вимкнення — обгорнути параметр у кортеж: [T] extends [U].
Дистрибутивність особливо корисна для створення типів-фільтрів і перетворення кожного варіанта union окремо.