Определение динамических типов при помощи условных выражений - очень важный момент описания сложных типизированных сценариев, на примере которых в данной главе будет рассмотрен механизм определения условных типов.
Условные типы (Conditional Types) — это типы, способные принимать одно из двух значений, основываясь на принадлежности одного типа к другому. Условные типы семантически схожи с тернарным оператором.
1
TextendsU?T1:T2
В блоке выражения с помощью ключевого слова extends устанавливается принадлежность к заданному типу. Если тип, указанный слева от ключевого слова extends, совместим с типом, указанным по правую сторону, то условный тип будет принадлежать к типу T1, иначе — к типу T2. Стоит заметить, что в качестве типов T1 и T2 могут выступать, в том числе и другие условные типы, что в свою очередь создаст цепочку условий определения типа.
Помимо того, что невозможно переоценить пользу от условных типов, очень сложно придумать минимальный пример, который бы эту пользу проиллюстрировал. Поэтому в этой главе будут приведены лишь бессмысленные примеры, демонстрирующие принцип их работы.
typeT0<T>=Textendsnumber?string:boolean;letv0:T0<5>;// let v0: stringletv1:T0<'text'>;// let v1: booleantypeT1<T>=Textendsnumber|string?object:never;letv2:T1<5>;// let v2: objectletv3:T1<'text'>;// let v3: objectletv4:T1<true>;// let v2: nevertypeT2<T>=Textendsnumber?'Ok':'Oops';letv5:T2<5>;// let v5: "Ok"letv6:T2<'text'>;// let v6: "oops"// вложенные условные типыtypeT3<T>=Textendsnumber?'IsNumber':Textendsstring?'IsString':'Oops';letv7:T3<5>;// let v7: "IsNumber"letv8:T3<'text'>;// let v8: "IsString"letv9:T3<true>;// let v9: "Opps"
Нужно быть внимательным, когда в условиях вложенных условных типов проверяются совместимые типы, так как порядок условий может повлиять на результат.
1 2 3 4 5 6 7 8 910111213141516171819202122232425
typeT0<T>=TextendsIAnimal?'animal':TextendsIBird?'bird':TextendsIRaven?'raven':'no animal';typeT1<T>=TextendsIRaven?'raven':TextendsIBird?'bird':TextendsIAnimal?'animal':'no animal';// всегда "animal"letv0:T0<IAnimal>;// let v0: "animal"letv1:T0<IBird>;// let v1: "animal"letv2:T0<IRaven>;// let v2: "animal"// никогда "bird"letv3:T1<IRaven>;// let v3: "raven"letv4:T1<IBird>;// let v4: "raven"letv5:T1<IAnimal>;// let v5: "animal"
Если в качестве аргумента условного типа выступает тип объединение (Union, глава Union, Intersection), то условия будут выполняться для каждого типа, составляющего объединенный тип.
interfaceIAnimal{type:string;}interfaceIBirdextendsIAnimal{fly():void;}interfaceIRavenextendsIBird{}typeT0<T>=TextendsIAnimal?'animal':TextendsIBird?'bird':TextendsIRaven?'raven':'no animal';typeT1<T>=TextendsIRaven?'raven':TextendsIBird?'bird':TextendsIAnimal?'animal':'no animal';// всегда "animal"letv0:T0<IAnimal|IBird>;// let v0: "animal"letv1:T0<IBird>;// let v1: "animal"letv2:T0<IRaven>;// let v2: "animal"// никогда "bird"letv3:T1<IAnimal|IRaven>;// let v3: "raven"letv4:T1<IBird>;// let v4: "raven"letv5:T1<IAnimal|IBird>;// let v5: "animal"
Помимо конкретного типа, в качестве правого (от ключевого слова extends) операнда также может выступать другой параметр типа.
Условные типы, которым в качестве аргумента типа устанавливается объединенный тип (Union Type, глава Union, Intersection), называются распределительные условные типы (Distributive Conditional Types). Называются они так, потому что каждый тип, составляющий объединенный тип, будет распределен таким образом, чтобы выражение условного типа было выполнено для каждого. Это, в свою очередь может определить условный тип как тип объединение.
12345678
typeT0<T>=Textendsnumber?'numeric':Textendsstring?'text':'other';letv0:T0<string|number>;// let v0: "numeric" | "text"letv1:T0<string|boolean>;// let v1: "text" | "other"
Для лучшего понимания процесса, происходящего при определении условного типа в случае, когда аргумент типа принадлежит к объединению, стоит рассмотреть следующий минимальный пример, в котором будет проиллюстрирован условный тип так, как его видит компилятор.
// так видит разработчикtypeT0<T>=Textendsnumber?'numeric':Textendsstring?'text':'other';letv0:T0<string|number>;// let v0: "numeric" | "text"letv1:T0<string|boolean>;// let v1: "text" | "other"// так видит компиляторtypeT0<T>=// получаем первый тип, составляющий union тип (в данном случае number) и начинаем подставлять его на место Tnumberextendsnumber?'numeric'// number соответствует number? Да! Определяем "numeric":Textendsstring?'text':|'other'// закончили определять один тип, приступаем к другому, в данном случае string|stringextendsnumber?'numeric'// string соответствует number? Нет! Продолжаем.:stringextendsstring?'text'// string соответствует string? Да! Определяем "text".:'other';// Итого: условный тип T0<string | number> определен, как "numeric" | "text"
Условные типы позволяют в блоке выражения объявлять переменные, тип которых будет устанавливать вывод типов. Переменная типа объявляется с помощью ключевого слова infer и, как уже говорилось, может быть объявлена исключительно в типе, указанном в блоке выражения, расположенном правее оператора extends.
Это очень простой механизм, который проще сразу рассмотреть на примере.
Предположим, что нужно установить, к какому типу принадлежит единственный параметр функции.
1
functionf(param:string):void{}
Для этого нужно создать условный тип, в условии которого происходит проверка на принадлежность к типу-функции. Кроме того, аннотация типа единственного параметра этой функции вместо конкретного типа будет содержать объявление переменной типа.
typeParamType<T>=Textends(p:inferU)=>void?U:undefined;functionf0(param:number):void{}functionf1(param:string):void{}functionf2():void{}functionf3(p0:number,p1:string):void{}functionf4(param:number[]):void{}letv0:ParamType<typeoff0>;// let v0: numberletv1:ParamType<typeoff1>;// let v1: stringletv2:ParamType<typeoff2>;// let v2: {}letv3:ParamType<typeoff3>;// let v3: undefinedletv4:ParamType<typeoff4>;// let v4: number[]. Oops, ожидалось тип number вместо number[]// определяем новый тип, чтобы разрешить последний случайtypeWithoutArrayParamType<T>=Textends(p:(inferU)[])=>void?U:Textends(p:inferU)=>void?U:undefined;letv5:WithoutArrayParamType<typeoff4>;// let v5: number. Ok
Принципы определения переменных в условных типах, продемонстрированные на примере функционального типа, идентичны и для объектных типов.
12345
typeParamType<T>=Textends{a:inferA;b:inferB}?A|B:undefined;letv:ParamType<{a:number;b:string}>;// let v: string | number