Пошук уроків, статей та іншого контенту
Створюватимете типи за умовою через синтаксис T extends U ? X : Y і комбінуватимете умовні перевірки.
Умовний тип вибирає один із двох типів залежно від того, чи відповідає один тип іншому.
T extends U ? X : YЦей запис читається так:
якщо T сумісний з U, результатом буде X;
інакше результатом буде Y.
Умовні типи обчислюються під час перевірки типів і не створюють JavaScript-код.
Найпростіший приклад:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
const a: A = true;
const b: B = false;extends в умовному типі перевіряє сумісність типів, а не буквальну ідентичність.
type HasId<T> = T extends { id: number } ? "так" : "ні";
type UserResult = HasId<{ id: number; name: string }>; // "так"
type TextResult = HasId<{ name: string }>; // "ні"Тип { id: number; name: string } містить усі властивості, необхідні для типу { id: number }, тому перевірка успішна.
Найчастіше умовні типи використовують разом із параметром типу:
type ArrayOrValue<T> = T extends unknown[] ? "масив" : "значення";
type First = ArrayOrValue<string[]>; // "масив"
type Second = ArrayOrValue<string>; // "значення"Умову можна використовувати для вибору фактичного типу:
type ToArray<T> = T extends unknown ? T[] : never;
type StringArray = ToArray<string>; // string[]
type NumberArray = ToArray<number>; // number[]У цьому випадку T завжди сумісний з unknown, тому результатом буде T[].
Практичніший приклад — перетворення типу залежно від його форми:
type MessageOf<T> = T extends { message: string }
? T["message"]
: never;
type ErrorMessage = MessageOf<{
message: string;
code: number;
}>; // string
type InvalidMessage = MessageOf<{
code: number;
}>; // neverЯкщо тип не має властивості message типу string, результатом стає never.
Умовні типи можна вкладати один в один. Це дає змогу описувати кілька варіантів.
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
"інший тип";
type A = TypeName<string>; // "string"
type B = TypeName<42>; // "number"
type C = TypeName<true>; // "boolean"
type D = TypeName<null>; // "інший тип"Перевірки виконуються зверху вниз. Для першого типу, який відповідає умові, вибирається відповідний результат.
Такий синтаксис можна форматувати як ланцюжок умов:
type Permission = "read" | "write" | "admin";
type PermissionDescription<T> =
T extends "admin" ? "повний доступ" :
T extends "write" ? "читання та запис" :
T extends "read" ? "лише читання" :
"невідомий дозвіл";
type AdminDescription = PermissionDescription<"admin">;
// "повний доступ"
type ReadDescription = PermissionDescription<"read">;
// "лише читання"Умовні типи можна комбінувати з об’єднаннями, перетинами та перевірками властивостей.
Наприклад, тип результату API може залежати від того, чи передано тип помилки:
type ApiResult<T> =
T extends { error: string }
? {
success: false;
error: string;
}
: {
success: true;
data: T;
};
type UserResponse = ApiResult<{
id: number;
name: string;
}>;
type ErrorResponse = ApiResult<{
error: string;
}>;Отримані типи:
type UserResponse = {
success: true;
data: {
id: number;
name: string;
};
};
type ErrorResponse = {
success: false;
error: string;
};Це можна використовувати у функціях:
type ApiResult<T> =
T extends { error: string }
? {
success: false;
error: string;
}
: {
success: true;
data: T;
};
type User = {
id: number;
name: string;
};
function handleUserResponse(
response: ApiResult<User>
): string {
if (response.success) {
return `Користувач: ${response.data.name}`;
}
return `Помилка: ${response.error}`;
}
const result = handleUserResponse({
success: true,
data: {
id: 1,
name: "Олена",
},
});
console.log(result);Умова визначає форму типу ApiResult<User>, тому TypeScript знає, що в успішній гілці є data.
Якщо умовний тип перевіряє безпосередньо параметр типу, TypeScript застосовує його до кожного елемента об’єднання окремо.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[]Тип обчислюється приблизно так:
type Result = ToArray<string> | ToArray<number>;
// string[] | number[]Ця поведінка називається розподілом умовного типу.
Ще один приклад:
type ExtractStrings<T> = T extends string ? T : never;
type Result = ExtractStrings<string | number | boolean>;
// stringОбчислення відбувається для кожного елемента:
type Result =
ExtractStrings<string> |
ExtractStrings<number> |
ExtractStrings<boolean>;
// string | never | never
// stringnever не додає нічого до об’єднання, тому залишається тільки string.
Іноді потрібно перевірити все об’єднання як один тип, а не кожен його елемент окремо.
Для цього параметр типу обгортають у кортеж:
type IsUnionAssignableToString<T> =
[T] extends [string] ? true : false;
type OneType = IsUnionAssignableToString<string>;
// true
type UnionType = IsUnionAssignableToString<string | number>;
// falseБез кортежу перевірка розподілялася б:
type Distributed<T> = T extends string ? true : false;
type Result = Distributed<string | number>;
// true | falseЗ кортежем перевіряється все значення string | number цілком:
type NotDistributed<T> = [T] extends [string] ? true : false;
type Result = NotDistributed<string | number>;
// falseЦей прийом також використовують для перевірки, чи є тип never:
type IsNever<T> = [T] extends [never] ? true : false;
type A = IsNever<never>; // true
type B = IsNever<string>; // falseКортеж важливий, оскільки звичайний варіант розподіляв би перевірку над never, і результат міг би бути неочікуваним.
Умовний тип може повертати не лише заздалегідь заданий тип, а й частину вхідного типу.
type ElementType<T> =
T extends readonly (infer Element)[] ? Element : T;
type FromStringArray = ElementType<string[]>; // string
type FromReadonlyArray = ElementType<readonly number[]>; // number
type FromString = ElementType<string>; // stringinfer Element оголошує локальний параметр типу. Якщо T є масивом, TypeScript виводить тип його елементів і повертає його.
Це також працює з функціями:
type FunctionResult<T> =
T extends (...args: never[]) => infer Result
? Result
: never;
type Result = FunctionResult<() => number>; // number
type AnotherResult = FunctionResult<(value: string) => boolean>; // booleanУмова перевіряє, чи є T функцією, а infer Result отримує тип її результату.
type Payload<T> =
T extends { data: infer Data }
? Data
: T extends { error: infer Error }
? Error
: never;
type UserPayload = Payload<{
data: {
id: number;
name: string;
};
}>;
type ErrorPayload = Payload<{
error: {
message: string;
code: number;
};
}>;
const user: UserPayload = {
id: 1,
name: "Олена",
};
const error: ErrorPayload = {
message: "Не знайдено",
code: 404,
};
console.log(user.name);
console.log(error.code);Тип Payload<T>:
перевіряє наявність властивості data;
якщо вона є, отримує її тип через infer Data;
якщо data немає, перевіряє властивість error;
якщо жодна умова не виконується, повертає never.
extends із наслідуванням класівУмовний тип не перевіряє, чи успадковується клас від іншого класу. Він перевіряє сумісність структур типів.
type HasName<T> = T extends { name: string } ? true : false;
type Result = HasName<{
name: string;
age: number;
}>; // trueДодаткові властивості не заважають сумісності.
type IsString<T> = T extends string ? true : false;
type Result = IsString<string | number>;
// true | falseЯкщо потрібно перевірити об’єднання цілком, використовуйте кортеж:
type IsEntirelyString<T> =
[T] extends [string] ? true : false;
type Result = IsEntirelyString<string | number>;
// falseУмова T extends unknown майже завжди істинна:
type AlwaysArray<T> = T extends unknown ? T[] : never;Така конструкція корисна для навмисного розподілу об’єднань, але не для перевірки конкретної властивості типу.
neverЯкщо тип не відповідає жодній умові, часто повертають never:
type OnlyNumbers<T> = T extends number ? T : never;
type Result = OnlyNumbers<string | number>;
// numbernever означає, що допустимих значень не залишилося. Це може бути правильним результатом, але його потрібно враховувати під час подальшого використання типу.
Умовний тип має форму T extends U ? X : Y.
Він вибирає тип X, якщо T сумісний з U, інакше вибирає Y.
Умови можна вкладати для перевірки кількох варіантів.
Для параметра типу, який безпосередньо перевіряється через extends, працює розподіл над об’єднаннями.
Щоб вимкнути розподіл, параметри обгортають у кортеж: [T] extends [U].
infer дає змогу отримати частину типу всередині умовного типу.
never часто використовують для варіантів, які не відповідають умові.