Условные типы (Conditional Types)

Определение динамических типов при помощи условных выражений, очень важный момент описания сложных типизированных сценариев, на примере которых, в данной главе будет рассмотрен механизм определения условных типов.

Условные типы на практике

Условные типы (Conditional Types) — это типы, способные принимать одно из двух значений, основываясь на принадлежности одного типу к другому. Условные типы семантически схожи с тернарным оператором.

ts
T extends U ? T1 : T2

В блоке выражения с помощью ключевого слова extends устанавливается принадлежность к заданному типу. Если тип, указанный слева от ключевого слова extends, совместим с типом, указанным по правую сторону, то условный тип будет принадлежать к типу T1, иначе — к типу T2. Стоит заметить, что в качестве типов T1 и T2 могут выступать, в том числе и другие условные типы, что в свою очередь создаст цепочку условий определения типа.

Помимо того, что невозможно переоценить пользу от условных типов, очень сложно придумать минимальный пример, который бы эту пользу проиллюстрировал. Поэтому в этой главе будут приведены лишь бессмысленные примеры, демонстрирующие принцип их работы.

ts
type T0<T> = T extends number ? string : boolean;

let v0: T0<5>; // let v0: string
let v1: T0<'text'>; // let v1: boolean

type T1<T> = T extends number | string ? object : never;

let v2: T1<5>; // let v2: object
let v3: T1<'text'>; // let v3: object
let v4: T1<true>; // let v2: never

type T2<T> = T extends number ? "Ok" : "Oops";

let v5: T2<5>; // let v5: "Ok"
let v6: T2<'text'>; // let v6: "oops"

// вложенные условные типы

type T3<T> =
    T extends number ? "IsNumber" :
    T extends string ? "IsString" :
    "Oops";

let v7: T3<5>; // let v7: "IsNumber"   
let v8: T3<'text'>; // let v8: "IsString"
let v9: T3<true>; // let v9: "Opps"

Нужно быть внимательным, когда в условиях вложенных условных типов проверяются совместимые типы, так как порядок условий может повлиять на результат.

ts
type T0<T> =
    T extends IAnimal ? "animal" :
    T extends IBird ? "bird" :
    T extends IRaven ? "raven" :
    "no animal";

type T1<T> =
    T extends IRaven ? "raven" :
    T extends IBird ? "bird" :
    T extends IAnimal ? "animal" :
    "no animal";

// всегда "animal"
let v0:T0<IAnimal>; // let v0: "animal"
let v1: T0<IBird>; // let v1: "animal"
let v2: T0<IRaven>; // let v2: "animal"

// никогда "bird"
let v3:T1<IRaven>; // let v3: "raven"
let v4: T1<IBird>; // let v4: "raven"
let v5: T1<IAnimal>; // let v5: "animal"

Если в качестве аргумента условного типа выступает тип объединение (Union, глава “Типы - Union, Intersection”), то условия будут выполняться для каждого типа, составляющего объединенный тип.

ts
interface IAnimal { type: string; }
interface IBird extends IAnimal { fly(): void; }
interface IRaven extends IBird {}

type T0<T> =
    T extends IAnimal ? "animal" :
    T extends IBird ? "bird" :
    T extends IRaven ? "raven" :
    "no animal";

type T1<T> =
    T extends IRaven ? "raven" :
    T extends IBird ? "bird" :
    T extends IAnimal ? "animal" :
    "no animal";

// всегда "animal"
let v0:T0<IAnimal | IBird>; // let v0: "animal"
let v1: T0<IBird>; // let v1: "animal"
let v2: T0<IRaven>; // let v2: "animal"

// никогда "bird"
let v3:T1<IAnimal | IRaven>; // let v3: "raven"
let v4: T1<IBird>; // let v4: "raven"
let v5: T1<IAnimal | IBird>; // let v5: "animal"

Помимо конкретного типа, в качестве правого (от ключевого слова extends) операнда также может выступать другой параметр типа.

ts
type T0<T, U> = T extends U ? "Ok" : "Oops";

let v0: T0<number, any>; // Ok
let v1:T0<number, string>; // Oops

Распределительные условные типы (Distributive Conditional Types)

Условные типы, которым в качестве аргумента типа устанавливается объединенный тип (Union Type, глава “Типы - Union, Intersection”), называются распределительные условные типы (Distributive Conditional Types). Называются они так, потому что каждый тип, составляющий объединенный тип, будет распределен таким образом, чтобы выражение условного типа было выполнено для каждого. Это, в свою очередь может определить условный тип, как тип объединение.

ts
type T0<T> =
    T extends number ? "numeric" :
    T extends string ? "text" :
    "other";

let v0: T0<string | number>; // let v0: "numeric" | "text"
let v1: T0<string | boolean>; // let v1: "text" | "other"

Для лучшего понимания процесса происходящего при определении условного типа в случае, когда аргумент типа принадлежит к объединению, стоит рассмотреть следующий минимальный пример, в котором будет проиллюстрирован условный тип так, как его видит компилятор.

ts
// так видит разработчик

type T0<T> =
    T extends number ? "numeric" :
    T extends string ? "text" :
    "other";

let v0: T0<string | number>; // let v0: "numeric" | "text"
let v1: T0<string | boolean>; // let v1: "text" | "other"

// так видит компилятор

type T0<T> =
  // получаем первый тип, составляющий union тип (в данном случае number) и начинаем подставлять его на место T

  number extends number ? "numeric" : // number соответствует number? Да! Определяем "numeric"
  T extends string ? "text" :
  "other"

  | // закончили определять один тип, приступаем к другому, в данном случае string

  string extends number ? "numeric" : // string соответствует number? Нет! Продолжаем.
  string extends string ? "text" : // string соответствует string? Да! Определяем "text".
  "other"

  // Итого: условный тип T0<string | number> определен, как "numeric" | "text"

Вывод типов в условном типе

Условные типы позволяют в блоке выражения объявлять переменные, тип которых будет устанавливать вывод типов. Переменная типа объявляется с помощью ключевого слова infer и, как уже говорилось, может быть объявлена исключительно в типе, указанном в блоке выражения расположенном правее оператора extends.

Это очень простой механизм, который проще сразу рассмотреть на примере.

Предположим что нужно установить, к какому типу принадлежит единственный параметр функции.

ts
function f(param: string): void {}

Для этого нужно создать условный тип, в условии которого происходит проверка на принадлежность к типу-функции. Кроме того, аннотация типа единственного параметра этой функции, вместо конкретного типа, будет содержать объявление переменной типа.

ts
type ParamType<T> = T extends (p: infer U) => void ? U : undefined;

function f0(param: number): void {}
function f1(param: string): void {}
function f2(): void {}
function f3(p0: number, p1: string): void {}
function f4(param: number[]): void {}

let v0: ParamType<typeof f0>; // let v0: number
let v1: ParamType<typeof f1>; // let v1: string
let v2: ParamType<typeof f2>; // let v2: {}
let v3: ParamType<typeof f3>; // let v3: undefined
let v4: ParamType<typeof f4>; // let v4: number[]. Oops, ожидалось тип number вместо number[]

// определяем новый тип, чтобы разрешить последний случай

type WithoutArrayParamType<T> =
    T extends (p: (infer U)[]) => void ? U :
    T extends (p: infer U) => void ? U :
    undefined;

 
let v5: WithoutArrayParamType<typeof f4>; // let v5: number. Ok

Принципы определения переменных в условных типах продемонстрированные на примере функционального типа, идентичны и для объектных типов.

ts
type ParamType<T> = T extends { a: infer A, b: infer B } ? A | B : undefined;

let v: ParamType<{ a: number, b:string }>; // let v: string | number