Spec-Zone.ru › TypeScript 5.1

Условные типы

В основе большинства полезных программ лежит принятие решений на основе входных данных. 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-функцию, которая делает выбор на основе типов её входных данных. Обратите внимание на несколько моментов:

  1. Если библиотеке нужно постоянно принимать такие решения в своём API, это становится неудобно.
  2. Нам нужно создать три перегрузки: по одной для каждого случая, когда мы уверены в типе (по одной для 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API