ジェネリクスとは型パラメーターのことです。ジェネリック関数はプレースホルダーの型(ふつうは T と呼びます)を宣言し、呼び出しのたびにそれが埋められるので、入る型が出てくる型を決めます。1つの関数が数値にも文字列にも自作のオブジェクトにも使え、しかもすべての呼び出しが完全に型付けされます。
関数名のあとの <T> が型パラメーターを宣言しています。入力には items: T[] として、出力には T | undefined として使い、2つを結び付けています。number や string とは一度も書いていません。TypeScriptが引数から T を推論しました。
any や unknown ではだめなのか
ジェネリクスを使わなければ、引数の型は any か unknown になります。どちらもあらゆる配列を受け付けますが、どちらも入力と出力のつながりを失います:
| 引数の型 | どんな入力も受け付けるか | 結果の型 | チェックされるか |
|---|---|---|---|
any | はい | any | いいえ: 間違いもコンパイルできる |
unknown | はい | unknown | はい、ただし使う前に絞り込みが必要 |
T(ジェネリクス) | はい | 入力と結び付く | はい、正確な型で |
型引数の推論
ふつうはTypeScriptが渡した値から型引数を推論します。呼び出し時に山かっこで明示的に書くこともでき、引数のどれも T に触れていない場合や、推論が望まない型を選ぶ場合にはそれが必要です。
知っておくべき推論の結果が2つあります。明示的な型引数なしの pair(1, "one") はエラーです。TypeScriptは最初の引数から T = number と推論し、Argument of type 'string' is not assignable to parameter of type 'number'.(TS2345)を報告します。そして引数がまったくない emptyList() は T = unknown になり、unknown[] が得られますが、これが望ましいことはめったにありません。
複数の型パラメーター
関数は必要なだけ型パラメーターをカンマ区切りで宣言できます。それぞれ独立して推論されます。
mapValues は3つを一度に推論します。K は "tea" | "cake"、V は number、R はコールバックの戻り値から string です。K extends string は制約で、次のページで扱います。
ジェネリックなインターフェースと型エイリアス
型もパラメーターを取れます。その型を使うときに引数を書きます: Box<number>、ApiResponse<User>。組み込みのコレクション型のほとんどはこの仕組みです: Array<T>、Map<K, V>、Promise<T>、Record<K, V>。
Result<T, E = string> はデフォルトの型パラメーターの例でもあります。Result<number> は Result<number, string> を意味します。省略可能な関数の引数と同じく、デフォルトのあるパラメーターは必須のパラメーターのあとに置きます。
ジェネリッククラス
クラスは名前のあとに型パラメーターを取り、インスタンスごとにそれが決まります。フィールド、メソッド、コンストラクターの引数のどれでも使えます。
コンストラクターが T を受け取るなら、明示的な型引数は省略できます。new Box(5) は Box<number> と推論されます。static メンバーはインスタンスではなくクラスそのものに属するので、クラスの型パラメーターを使えません(Static members cannot reference class type parameters.、TS2302)。
ジェネリックなアロー関数と関数型
型パラメーターのリストは引数リストの前に書きます。ジェネリックな関数型も同じ構文で表します。
const last = <T>(items: T[]): T | undefined => items[items.length - 1];
type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);
// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];
const 型パラメーターでリテラル型を推論する
デフォルトでは、ジェネリクスは広げた型を推論します。pair("a", "b") は [string, string] になります。正確な値が重要な場合(ルート名、イベント名、カラムの一覧)は、パラメーターに const を付けると(TypeScript 5.0 以降)、引数を as const で書いたかのように推論されます:
呼び出し側は普通の配列を書き、関数はリテラル型を保ちます。
ジェネリクスを使わないほうがよい場面
型パラメーターが役に立つのは、2つのものを結び付けるときです。引数と戻り値の型、2つの引数、引数とコールバックなどです。T が一度しか出てこないなら何も加えていないので、普通の型のほうがわかりやすくなります。
// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
console.log(value);
}
// Clearer
function logValueSimple(value: unknown): void {
console.log(value);
}
// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
return JSON.parse(text);
}
最後の例はよく見かけますが、誤解を招きます。parseJson<User>(text) は型安全に見えますが、JSON.parse(text) as User とまったく同じで何もチェックしません。unknown を返して検証しましょう。ほかにも使いすぎのサインがあります。いつも同じ型引数が渡される型パラメーターや、既知の2つの型のユニオン型で置き換えられる型パラメーターです。
T について何かを知る必要があるジェネリクス(length を持つ、K が T のキーである、など)には、次のステップとして extends による制約を使います。
よくある質問
TypeScriptのジェネリクスとは何ですか?
ジェネリクスとは型パラメーターのことです。T のようなプレースホルダーで、関数、インターフェース、型エイリアス、クラスを使うたびに実際の型で埋められます。function first<T>(items: T[]): T | undefined はどんな配列にも使え、結果は渡した配列の要素の型になるので、first([1, 2]) は number | undefined、first(["a"]) は string | undefined です。
ジェネリクスと any の違いは何ですか?
any は型チェックを止めます。入る値と出る値に関係がなく、結果も any になります。ジェネリクスは関係を保ちます。identity<T>(x: T): T は渡した型をそのまま返します。何でも受け付けるが型を返す必要がないなら unknown を、型を返す必要があるならジェネリクスを使いましょう。
TypeScriptの <T> はどういう意味ですか?
T という名前の型パラメーターを宣言しています。function wrap<T>(value: T) の T は型変数で、TypeScriptが呼び出しのたびに引数から埋めるか、wrap<string>("a") のように明示的に渡します。T という名前は慣習にすぎず、TItem や Key のような長い名前でも同じように動きます。
TypeScriptでジェネリックなアロー関数を書くには?
型パラメーターのリストを引数の前に書きます: const first = <T>(items: T[]): T | undefined => items[0];。.tsx ファイルでは <T> がJSXのタグとして読まれてしまうので、末尾にカンマを付けた <T,> か、<T extends unknown> と書きます。
TypeScriptでジェネリクスにデフォルトの型を設定するには?
パラメーターのあとに = Type を付けます: interface ApiResponse<T = unknown> { data: T }。引数なしの ApiResponse は ApiResponse<unknown> を意味します。省略可能な関数の引数と同じく、デフォルトのあるパラメーターはデフォルトのないパラメーターのあとに置きます。