Условные типы
В основе большинства полезных программ лежит принятие решений на основе входных данных. JavaScript-программы не являются исключением, но, учитывая возможность лёгкого ознакомления со значениями, эти решения также основаны на типах входных данных. Условные типы помогают описать взаимосвязь между типами входных и выходных данных.
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
type Example2 = RegExp extends Animal ? number : string; Условные типы имеют форму, немного похожую на условные выражения (condition ? trueExpression : falseExpression) в JavaScript:
SomeType extends OtherType ? TrueType : FalseType;
Если тип слева от extends присваивается типу справа, то вы получите тип в первой ветви (ветви «истина»); в противном случае вы получите тип во второй ветви (ветви «ложь»).
Исходя из приведенных примеров, условные типы могут не сразу показаться полезными — мы можем сами определить, является ли Dog extends Animal применимым, и выбрать number или string! Но мощь условных типов заключается в их использовании с обобщениями.
Например, рассмотрим следующую createLabel функцию:
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
} Эти перегрузки для createLabel описывают единственную JavaScript-функцию, которая делает выбор на основе типов её входных данных. Обратите внимание на несколько моментов:
- Если библиотеке нужно постоянно принимать такие решения в своём API, это становится неудобно.
- Нам нужно создать три перегрузки: по одной для каждого случая, когда мы уверены в типе (по одной для
stringи по одной дляnumber), и одну для наиболее общего случая (принимающуюstring | number). При каждом новом типе, который может обработатьcreateLabel, количество перегрузок растёт экспоненциально.
Вместо этого мы можем закодировать эту логику в условном типе:
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
Затем мы можем использовать этот условный тип для упрощения перегрузок до единственной функции без перегрузок.
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
let b = createLabel(2.8);
let c = createLabel(Math.random() ? "hello" : 42); Ограничения условных типов
Часто проверки в условном типе предоставляют нам новую информацию. Так же, как сужение с помощью сторожей типов может дать нам более конкретный тип, ветвь «истина» условного типа дополнительно ограничит обобщения типом, с которым мы сравниваем.
Например, рассмотрим следующее:
type MessageOf<T> = T["message"];
В этом примере TypeScript выдаёт ошибку, потому что T не известно, что обладает свойством message. Мы могли бы ограничить T, и TypeScript больше не будет жаловаться:
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>; Однако что, если мы хотим, чтобы MessageOf принимал любой тип и по умолчанию возвращал что-то вроде never если свойство message недоступно? Это можно сделать, вынеся ограничение наружу и введя условный тип:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
type DogMessageContents = MessageOf<Dog>; В ветви «истина» TypeScript знает, что T будет обладать свойством message.
В качестве ещё одного примера, мы можем написать тип под названием Flatten, который преобразует массивы в типы их элементов, но оставит другие типы без изменений:
type Flatten<T> = T extends any[] ? T[number] : T; // Extracts out the element type. type Str = Flatten<string[]>; // Leaves the type alone. type Num = Flatten<number>;
Когда Flatten получает тип массива, он использует индексированный доступ с number для извлечения типа элемента string[]. В противном случае он просто возвращает предоставленный тип.
Вывод внутри условных типов
Мы только что использовали условные типы для применения ограничений и последующего извлечения типов. Это оказывается настолько распространённой операцией, что условные типы делают её проще.
Условные типы предоставляют нам способ вывода из типов, с которыми мы сравниваем в ветви «истина», используя ключевое слово infer. Например, мы могли бы вывести тип элемента в Flatten вместо того, чтобы извлекать его «вручную» с помощью индексированного типа доступа:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Здесь мы использовали ключевое слово infer для декларативного введения новой переменной обобщения под названием Item вместо указания способа извлечения типа элемента Type в ветви «истина». Это освобождает нас от необходимости думать о том, как пройти через и исследовать структуру типов, которые нас интересуют.
Мы можем написать полезные псевдонимы типов с помощью ключевого слова infer. Например, в простых случаях мы можем извлечь тип возвращаемого значения из типов функций:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return ? Return : never; type Num = GetReturnType<() => number>; type Str = GetReturnType<(x: string) => string>; type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
При выводе из типов с несколькими сигнатурами вызова (такими как тип перегруженной функции), вывод осуществляется из последней сигнатуры (которая, предположительно, является наиболее общей). Невозможно выполнить разрешение перегрузок на основе списка типов аргументов.
declare function stringOrNum(x: string): number; declare function stringOrNum(x: number): string; declare function stringOrNum(x: string | number): string | number; type T1 = ReturnType<typeof stringOrNum>;
Распределительные условные типы
Когда условные типы действуют над обобщенным типом, они становятся распределительными при использовании типа объединения. Например, рассмотрим следующее:
type ToArray<Type> = Type extends any ? Type[] : never;
Если мы подставим тип объединения в ToArray, то условный тип будет применён к каждому члену этого объединения.
type ToArray<Type> = Type extends any ? Type[] : never; type StrArrOrNumArr = ToArray<string | number>;
Что происходит здесь, так это то, что ToArray распределяется по:
string | number;
и применяется к каждому типу члена объединения, что фактически эквивалентно:
ToArray<string> | ToArray<number>;
что приводит к:
string[] | number[];
Как правило, распределительность является желаемым поведением. Чтобы избежать этого поведения, можно заключить каждую сторону ключевого слова extends в квадратные скобки.
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; // 'StrArrOrNumArr' is no longer a union. type StrArrOrNumArr = ToArrayNonDist<string | number>;
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html