Menu

TypeScriptのジェネリクス: 関数・型・クラスでの使い方

ジェネリクスを使うと、関数、インターフェース、型、クラスが多くの型に対応しつつ、型どうしのつながりを保てます。入れた型が出てくる型を決めます。ジェネリック関数、型引数の推論、複数の型パラメーター、ジェネリックなインターフェースとクラス、デフォルトの型、ジェネリクスを使わないほうがよい場面を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

ジェネリクスとは型パラメーターのことです。ジェネリック関数はプレースホルダーの型(ふつうは 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> を意味します。省略可能な関数の引数と同じく、デフォルトのあるパラメーターはデフォルトのないパラメーターのあとに置きます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める