条件型

ほとんどの便利なプログラムの中心には、入力に基づいて判断を下す必要があります。JavaScriptプログラムも例外ではありませんが、値は簡単にイントロスペクトできるため、それらの判断は入力の型にも基づいています。条件型は、入力と出力の型の関係を記述するのに役立ちます。

ts
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string;
type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;
type Example2 = string
Try

条件型は、JavaScriptの条件式(condition ? trueExpression : falseExpression)に少し似た形式をとります。

ts
SomeType extends OtherType ? TrueType : FalseType;
Try

extendsの左側の型が右側の型に割り当て可能な場合、最初の分岐(「true」分岐)の型が取得されます。それ以外の場合は、後者の分岐(「false」分岐)の型が取得されます。

上記の例から、条件型はすぐに役立つようには見えないかもしれません。Dog extends Animalかどうかを自分で判断し、numberまたはstringを選択できます!しかし、条件型の力は、ジェネリクスで使用するところから来ています。

たとえば、次のcreateLabel関数を見てみましょう。

ts
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";
}
Try

createLabelのこれらのオーバーロードは、入力の型に基づいて選択を行う単一のJavaScript関数を記述しています。いくつかの点に注意してください。

  1. ライブラリがAPI全体で同じ種類の選択を何度も行う必要がある場合、これは面倒になります。
  2. タイプを確信している場合(stringの場合とnumberの場合)、および最も一般的なケース(string | numberを受け取る)の場合、3つのオーバーロードを作成する必要があります。createLabelが処理できる新しい型ごとに、オーバーロードの数は指数関数的に増加します。

代わりに、そのロジックを条件型でエンコードできます。

ts
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
Try

次に、その条件型を使用して、オーバーロードなしで、単一の関数にオーバーロードを簡略化できます。

ts
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
 
let a = createLabel("typescript");
let a: NameLabel
 
let b = createLabel(2.8);
let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);
let c: NameLabel | IdLabel
Try

条件型制約

多くの場合、条件型のチェックは、いくつかの新しい情報を提供してくれます。タイプガードによる絞り込みがより具体的な型を提供できるのと同様に、条件型の真の分岐は、チェックする型によってジェネリクスをさらに制約します。

たとえば、次のようなものを見てみましょう。

ts
type MessageOf<T> = T["message"];
Type '"message"' cannot be used to index type 'T'.2536Type '"message"' cannot be used to index type 'T'.
Try

この例では、Tmessageというプロパティがあることがわかっていないため、TypeScriptはエラーを出します。Tを制約すると、TypeScriptは文句を言わなくなります。

ts
type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
message: string;
}
 
type EmailMessageContents = MessageOf<Email>;
type EmailMessageContents = string
Try

ただし、MessageOfが任意の型を受け取り、messageプロパティが使用できない場合は、neverのようなものにデフォルト設定したい場合はどうすればよいでしょうか?制約を外して条件型を導入することで、これを行うことができます。

ts
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
message: string;
}
 
interface Dog {
bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;
type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;
type DogMessageContents = never
Try

trueブランチ内では、TypeScriptはTmessageプロパティがあることを認識しています。

別の例として、配列型を要素型にフラット化し、それ以外の場合はそのままにするFlattenという型を作成することもできます。

ts
type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[]>;
type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>;
type Num = number
Try

Flattenに配列型が与えられると、numberでインデックスアクセスを使用して、string[]の要素型をフェッチします。それ以外の場合は、与えられた型をそのまま返します。

条件型内での推論

条件型を使って制約を適用し、型を抽出するという場面に遭遇しました。これは非常に一般的な操作であるため、条件型を使うことで簡単にできるようになります。

条件型は、inferキーワードを使って、真の分岐で比較対象となる型から推論する方法を提供します。例えば、Flattenの中で、インデックスアクセス型を使って「手動で」要素型を取り出す代わりに、要素型を推論することができます。

ts
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
Try

ここでは、真の分岐内でTypeの要素型をどのように取得するかを指定する代わりに、inferキーワードを使って、Itemという名前の新しいジェネリック型変数を宣言的に導入しました。これにより、対象の型の構造を掘り下げて調べる方法を考える必要がなくなります。

inferキーワードを使って、便利なヘルパー型エイリアスをいくつか記述できます。例えば、単純なケースでは、関数型から戻り値の型を抽出できます。

ts
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
 
type Num = GetReturnType<() => number>;
type Num = number
 
type Str = GetReturnType<(x: string) => string>;
type Str = string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
type Bools = boolean[]
Try

複数の呼び出しシグネチャを持つ型(オーバーロードされた関数の型など)から推論する場合、推論は最後のシグネチャ(おそらく、最も許容的な包括的なケース)から行われます。引数の型リストに基づいてオーバーロード解決を行うことはできません。

ts
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 T1 = string | number
Try

分配条件型

条件型がジェネリック型に対して作用する場合、ユニオン型が与えられると分配されます。例えば、次のような例を考えてみましょう。

ts
type ToArray<Type> = Type extends any ? Type[] : never;
Try

ユニオン型をToArrayに渡すと、条件型はそのユニオンの各メンバーに適用されます。

ts
type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>;
type StrArrOrNumArr = string[] | number[]
Try

ここで起こることは、ToArrayが次のように分配されるということです。

ts
string | number;
Try

そして、ユニオンの各メンバー型をマッピングし、結果的に次のようになります。

ts
ToArray<string> | ToArray<number>;
Try

これにより、最終的に次の結果が得られます。

ts
string[] | number[];
Try

通常、分配性は望ましい動作です。その動作を回避するには、extendsキーワードの両側を角括弧で囲むことができます。

ts
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
type ArrOfStrOrNum = (string | number)[]
Try

TypeScriptドキュメントはオープンソースプロジェクトです。これらのページの改善にご協力ください。 プルリクエストを送ってください

このページの貢献者
OTOrta Therox (10)
BKBenedikt König (1)
GFGeorge Flinn (1)
SFShinya Fujino (1)
NMNicolás Montone (1)
9+

最終更新日: 2024年3月21日