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