ジェネリクスの制約は、型パラメーターがなれる型を制限します。<T extends { length: number }> は数値の length を持つ型(文字列、配列、自作のオブジェクト)だけを受け付け、その約束があるので関数は .length を読めます。
戻り値の型は { length: number } ではなく T のままです。数値の配列を2つ渡せば number[] が返り、オブジェクトは tag を保ちます。制約は最低限の条件を表し、呼び出し側の型は保たれます。
制約が必要な理由
ジェネリック関数の中では、制約のない T は文字列、数値、null、関数など何でもありえます。そのためTypeScriptはすべての型で動く操作だけを許可しますが、それはほとんど何もないのと同じです。
コンパイラーの出力は次のとおりです:
index.ts(3,12): error TS2339: Property 'length' does not exist on type 'T'.
index.ts(3,24): error TS2339: Property 'length' does not exist on type 'T'.
必要なものを書けば解決します: <T extends { length: number }>。制約は「どんな型でも」を「これを持つどんな型でも」に変えます。ジェネリック関数が意図しているのはたいていこちらです。
インターフェースへの制約
制約にはどんな型でも使え、すでにあるインターフェースや型エイリアスも使えます。そうすると関数は少なくともそのメンバーを持つものなら何でも扱え、しかも呼び出し側の完全な型を返します。
普通の引数の型と比べてみましょう。function byId(items: HasId[], id: number): HasId | undefined は同じ配列を受け付けますが、結果は HasId だけなので、found.name はエラーになります。ジェネリックな版は完全な型をそのまま通します。
K extends keyof T: プロパティへの安全なアクセス
いちばんよく使う制約は、2つの型パラメーターを関係づけるものです。K extends keyof T は K が T のプロパティ名のどれかであることを表し、T[K] はそのプロパティの型です。
キーを打ち間違えたり存在しないキーを使ったりするとコンパイルエラーになり、戻り値の型はキーに従います。"year" なら number、"title" なら string です。keyof そのもの(インデックスシグネチャとの組み合わせでの動きを含む)は keyof のページで説明しています。
プリミティブへの制約
制約はプリミティブやそのユニオン型でもかまいません。便利な副作用があります。T extends string(または number)のとき、TypeScriptは引数の型を広げずにリテラル型を推論します。
制約がなければ、createEvent("user.saved") は { type: string } を返します。この場合TypeScriptは T に推論したリテラルを string に広げるからです。T extends string があればリテラルが保たれます。型付きのイベントやルーティングのヘルパーが、普通の文字列の引数から正確な名前を得られるのはこの仕組みです。
よくあるエラー
制約に合うだけの値を返す。 T を返す関数は、制約に合うだけの値ではなく T を返さなければなりません:
interface HasId {
id: number;
}
function reset<T extends HasId>(item: T): T {
return { id: item.id };
}
// error TS2322: Type '{ id: number; }' is not assignable to type 'T'.
// '{ id: number; }' is assignable to the constraint of type 'T', but 'T' could be
// instantiated with a different subtype of constraint 'HasId'.
T が { id: number; name: string } なら、新しいオブジェクトには name がないので T ではありません。すべてのプロパティを保つように入力をスプレッドして返す(return { ...item, id: 0 };)か、作るものがそれだけなら戻り値の型を HasId と宣言しましょう。
制約を破る型引数を渡す。 明示的な型引数もチェックされます。function scale<T extends number>(x: T) に対する scale<string>("2") という呼び出しは Type 'string' does not satisfy the constraint 'number'.(TS2344)で失敗します。
ジェネリクスが不要なのに制約を付ける。 T が引数にしか出てこないなら、function print<T extends HasId>(item: T): void は function print(item: HasId): void に余計な構文を足しただけです。制約が意味を持つのは、T が戻り値の型やほかの引数にも出てくるときです。
制約とデフォルトの組み合わせ
型パラメーターは制約とデフォルトの両方を持てます。デフォルトは制約を満たしていなければなりません。
interface Store<TState extends object = Record<string, unknown>> {
get(): TState;
set(next: Partial<TState>): void;
}
type AnyStore = Store; // Store<Record<string, unknown>>
type CounterStore = Store<{ count: number }>; // fine
type BadStore = Store<number>;
// error TS2344: Type 'number' does not satisfy the constraint 'object'.
同じ extends キーワードは条件型 T extends string ? A : B にも出てきますが、そこでは制限ではなくテストです。この形は 条件型 のページで扱います。
よくある質問
TypeScriptのジェネリクスの T extends はどういう意味ですか?
<T extends Constraint> は、T が Constraint に代入できる任意の型であるという意味です。呼び出し側はそうした型しか渡せず、関数の中では制約が保証するものをすべて使えます。<T extends { length: number }> は文字列、配列、数値の length を持つ任意のオブジェクトを受け付け、value.length を読めるようにします。
K extends keyof T はどういう意味ですか?
K は T のプロパティ名のどれかでなければならない、という意味です。インデックスアクセス型 T[K] と組み合わせると、プロパティへのアクセスを正確に型付けできます。function get<T, K extends keyof T>(obj: T, key: K): T[K] は obj の実在するキーだけを受け付け、戻り値の型はそのプロパティの型になります。
「Property does not exist on type T」と言われるのはなぜですか?
制約のない T は number や null を含むどんな型でもありうるので、TypeScriptは特定の型に依存する操作を何も許可しません(TS2339)。プロパティを保証する制約を追加しましょう: <T extends { name: string }>、またはすでにあるインターフェースへの制約 <T extends User> です。
「T could be instantiated with a different subtype of constraint」とはどういう意味ですか?
エラー TS2322 で、関数は T を返すと約束しているのに、制約に合うだけの値を返しているという意味です。T が { id: number; name: string } なら、新しく作った { id: 1 } は T ではありません。受け取った値(またはそれをスプレッドしたもの)を返すか、戻り値の型を制約の型に変えましょう。
ジェネリクスの extends とクラスやインターフェースの extends の違いは何ですか?
同じキーワードで、考え方も近いものです。class Dog extends Animal や interface B extends A では継承を宣言します。<T extends A> では制約を宣言し、T は A に代入できなければなりません。条件型 T extends A ? X : Y では、2つの型のどちらかを選ぶテストです。