ほとんどの便利なプログラムの中心には、入力に基づいて判断を下す必要があります。JavaScriptプログラムも例外ではありませんが、値は簡単にイントロスペクトできるため、それらの判断は入力の型にも基づいています。条件型は、入力と出力の型の関係を記述するのに役立ちます。
tsTryinterfaceAnimal {live (): void;}interfaceDog extendsAnimal {woof (): void;}typeExample1 =Dog extendsAnimal ? number : string;typeExample2 =RegExp extendsAnimal ? number : string;
条件型は、JavaScriptの条件式(condition ? trueExpression : falseExpression)に少し似た形式をとります。
tsTrySomeType extendsOtherType ?TrueType :FalseType ;
extendsの左側の型が右側の型に割り当て可能な場合、最初の分岐(「true」分岐)の型が取得されます。それ以外の場合は、後者の分岐(「false」分岐)の型が取得されます。
上記の例から、条件型はすぐに役立つようには見えないかもしれません。Dog extends Animalかどうかを自分で判断し、numberまたはstringを選択できます!しかし、条件型の力は、ジェネリクスで使用するところから来ています。
たとえば、次のcreateLabel関数を見てみましょう。
tsTryinterfaceIdLabel {id : number /* some fields */;}interfaceNameLabel {name : string /* other fields */;}functioncreateLabel (id : number):IdLabel ;functioncreateLabel (name : string):NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel ;functioncreateLabel (nameOrId : string | number):IdLabel |NameLabel {throw "unimplemented";}
createLabelのこれらのオーバーロードは、入力の型に基づいて選択を行う単一のJavaScript関数を記述しています。いくつかの点に注意してください。
- ライブラリがAPI全体で同じ種類の選択を何度も行う必要がある場合、これは面倒になります。
- タイプを確信している場合(
stringの場合とnumberの場合)、および最も一般的なケース(string | numberを受け取る)の場合、3つのオーバーロードを作成する必要があります。createLabelが処理できる新しい型ごとに、オーバーロードの数は指数関数的に増加します。
代わりに、そのロジックを条件型でエンコードできます。
tsTrytypeNameOrId <T extends number | string> =T extends number?IdLabel :NameLabel ;
次に、その条件型を使用して、オーバーロードなしで、単一の関数にオーバーロードを簡略化できます。
tsTryfunctioncreateLabel <T extends number | string>(idOrName :T ):NameOrId <T > {throw "unimplemented";}leta =createLabel ("typescript");letb =createLabel (2.8);letc =createLabel (Math .random () ? "hello" : 42);
条件型制約
多くの場合、条件型のチェックは、いくつかの新しい情報を提供してくれます。タイプガードによる絞り込みがより具体的な型を提供できるのと同様に、条件型の真の分岐は、チェックする型によってジェネリクスをさらに制約します。
たとえば、次のようなものを見てみましょう。
tsTrytypeType '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.MessageOf <T > =T ["message"];
この例では、Tにmessageというプロパティがあることがわかっていないため、TypeScriptはエラーを出します。Tを制約すると、TypeScriptは文句を言わなくなります。
tsTrytypeMessageOf <T extends {message : unknown }> =T ["message"];interfacemessage : string;}typeEmailMessageContents =MessageOf <
ただし、MessageOfが任意の型を受け取り、messageプロパティが使用できない場合は、neverのようなものにデフォルト設定したい場合はどうすればよいでしょうか?制約を外して条件型を導入することで、これを行うことができます。
tsTrytypeMessageOf <T > =T extends {message : unknown } ?T ["message"] : never;interfacemessage : string;}interfaceDog {bark (): void;}typeEmailMessageContents =MessageOf <typeDogMessageContents =MessageOf <Dog >;
trueブランチ内では、TypeScriptはTにmessageプロパティがあることを認識しています。
別の例として、配列型を要素型にフラット化し、それ以外の場合はそのままにするFlattenという型を作成することもできます。
tsTrytypeFlatten <T > =T extends any[] ?T [number] :T ;// Extracts out the element type.typeStr =Flatten <string[]>;// Leaves the type alone.typeNum =Flatten <number>;
Flattenに配列型が与えられると、numberでインデックスアクセスを使用して、string[]の要素型をフェッチします。それ以外の場合は、与えられた型をそのまま返します。
条件型内での推論
条件型を使って制約を適用し、型を抽出するという場面に遭遇しました。これは非常に一般的な操作であるため、条件型を使うことで簡単にできるようになります。
条件型は、inferキーワードを使って、真の分岐で比較対象となる型から推論する方法を提供します。例えば、Flattenの中で、インデックスアクセス型を使って「手動で」要素型を取り出す代わりに、要素型を推論することができます。
tsTrytypeFlatten <Type > =Type extendsArray <inferItem > ?Item :Type ;
ここでは、真の分岐内でTypeの要素型をどのように取得するかを指定する代わりに、inferキーワードを使って、Itemという名前の新しいジェネリック型変数を宣言的に導入しました。これにより、対象の型の構造を掘り下げて調べる方法を考える必要がなくなります。
inferキーワードを使って、便利なヘルパー型エイリアスをいくつか記述できます。例えば、単純なケースでは、関数型から戻り値の型を抽出できます。
tsTrytypeGetReturnType <Type > =Type extends (...args : never[]) => inferReturn ?Return : never;typeNum =GetReturnType <() => number>;typeStr =GetReturnType <(x : string) => string>;typeBools =GetReturnType <(a : boolean,b : boolean) => boolean[]>;
複数の呼び出しシグネチャを持つ型(オーバーロードされた関数の型など)から推論する場合、推論は最後のシグネチャ(おそらく、最も許容的な包括的なケース)から行われます。引数の型リストに基づいてオーバーロード解決を行うことはできません。
tsTrydeclare functionstringOrNum (x : string): number;declare functionstringOrNum (x : number): string;declare functionstringOrNum (x : string | number): string | number;typeT1 =ReturnType <typeofstringOrNum >;
分配条件型
条件型がジェネリック型に対して作用する場合、ユニオン型が与えられると分配されます。例えば、次のような例を考えてみましょう。
tsTrytypeToArray <Type > =Type extends any ?Type [] : never;
ユニオン型をToArrayに渡すと、条件型はそのユニオンの各メンバーに適用されます。
tsTrytypeToArray <Type > =Type extends any ?Type [] : never;typeStrArrOrNumArr =ToArray <string | number>;
ここで起こることは、ToArrayが次のように分配されるということです。
tsTrystring | number;
そして、ユニオンの各メンバー型をマッピングし、結果的に次のようになります。
tsTryToArray <string> |ToArray <number>;
これにより、最終的に次の結果が得られます。
tsTrystring[] | number[];
通常、分配性は望ましい動作です。その動作を回避するには、extendsキーワードの両側を角括弧で囲むことができます。
tsTrytypeToArrayNonDist <Type > = [Type ] extends [any] ?Type [] : never;// 'ArrOfStrOrNum' is no longer a union.typeArrOfStrOrNum =ToArrayNonDist <string | number>;