TypeScriptのジェネリクスとは?型引数の意味や使い方をわかりやすく解説!

TypeScriptを使っていると、次のようなTが入ったコードを目にすることがあります。

type User<T> = {
  id: T;
  name: string;
};

初めて見ると、

  • <T>とは何?
  • Tには何が入るの?
  • なぜ普通にstringやnumberと書かないの?
  • ジェネリクスと型引数は何が違うの?
  • 型アサーションで使う<Hoge>とは同じもの?

と疑問に思うこともあるでしょう。

この<T>は、TypeScriptの「ジェネリクス(Generics)」で使用する書き方です。ジェネリクスを使用すると、使用する型をあとから決められるため、同じ処理や型定義をさまざまな型で再利用できます。

この記事では、TypeScriptのジェネリクスとは何か、<T>の意味、型パラメーター・型引数の違いから、関数や型定義での使い方まで、わかりやすく解説します。

TypeScriptのジェネリクスとは?

ジェネリクス(Generics)とは、使用する型を最初から1つに固定せず、利用するときに型を決められる仕組みです。型は明示的に指定するだけでなく、TypeScriptによって推論される場合もあります。

例えば、次のような型があるとします。

type User = {
  id: string;
  name: string;
};

この型では、idの型がstringに固定されています。

そのため、次のように文字列のIDを持つユーザーは作成できます。

const userOne: User = {
  id: "U001",
  name: "太郎",
};

しかし、同じUser型で数値のIDも扱いたい場合、idがstring型に固定されているため、そのままでは使用できません。

例えば、次のコードはエラーになります。

const userTwo: User = {
  id: 2002,
  name: "次郎",
};

このようなときに役立つのがジェネリクスです。ジェネリクスを使ってidの型をあとから指定できるようにしてみましょう。

type User<T> = {
  id: T;
  name: string;
};

Tには、Userを実際に使用するときに型を指定します。

文字列のIDを扱う場合は、次のように記述します。

const userOne: User<string> = {
  id: "U001",
  name: "太郎",
};

数値のIDを扱う場合は、次のように記述します。

const userTwo: User<number> = {
  id: 2002,
  name: "次郎",
};

このように、User<string>と指定するとTはstringになり、User<number>と指定するとTはnumberになります。

つまり、ジェネリクスを簡単にいうと、「ここで使う型は、実際に使用するときに決めます」としておくための仕組みです。

<T>とは?

先ほどのコードをもう一度見てみましょう。

type User<T> = {
  id: T;
  name: string;
};

このTは、あとから指定または推論される型を受け取るための型パラメーターです。

Tという名前に決まりがあるわけではなく、別の名前を使用することもできます。

例えば、次のようにIdTypeと書いても同じように使用できます。

type User<IdType> = {
  id: IdType;
  name: string;
};

一般的には、Typeを意味するTがよく使用されます。

また、複数の型パラメーターを使用する場合は、T、U、Kなどの名前が使われることがあります。

type Pair<T, U> = {
  first: T;
  second: U;
};

一方、型の役割を分かりやすくしたい場合は、意味のある名前を付けることもできます。

例えば、APIから取得したデータを表す型では、次のようにDataという名前を使用できます。

type ApiResponse<Data> = {
  data: Data;
};

短いコードではTのような短い名前でも分かりやすいですが、複雑なコードではDataやItem、IdTypeなど、役割が分かる名前を使用すると読みやすくなる場合があります。

「型パラメーター」と「型引数」の違い

ジェネリクスについて調べていると、「型パラメーター」と「型引数」という言葉を目にすることがあります。

例えば、次のコードを見てみましょう。

type User<T> = {
  id: T;
  name: string;
};

const userOne: User<string> = {
  id: "U001",
  name: "太郎",
};

厳密には、それぞれ次のように考えます。

用語該当部分意味
型パラメーターTあとから型を受け取るために定義するもの
型引数string実際に指定する型

つまり、type User<T>のTは「ここに型を受け取ります」という型パラメーターです。

一方、User<string>のstringは、実際に渡している型引数です。

通常の関数で考えると理解しやすくなります。

例えば、次の関数ではnameが引数を受け取るためのパラメーターです。

function hello(name: string) {
  console.log(name);
}

hello("Taro");

ジェネリクスも考え方は似ています。

通常の関数
name ← "Taro"

ジェネリクス:
T ← string

値ではなく、型を受け渡していると考えると分かりやすいでしょう。

なお、日本語の記事や会話ではT自体を「型引数」と呼ぶ場合もありますが、TypeScriptの用語として区別する場合は、Tが型パラメーター、実際に指定するstringなどが型引数です。

ジェネリクスは何に使う?

ジェネリクスは、データの形や処理は同じだけれど、使用する型だけが異なる場合に便利です。

先ほどのUser型では、nameは常にstring型ですが、idについては文字列を使用する場合もあれば、数値を使用する場合もあります。

ジェネリクスを使わない場合、次のように別々の型を作ることもできます。

type StringIdUser = {
  id: string;
  name: string;
};

type NumberIdUser = {
  id: number;
  name: string;
};

しかし、2つの型の違いはidの型だけです。

そこで、ジェネリクスを使用すると次のように1つの型にまとめられます。

type User<T> = {
  id: T;
  name: string;
};

Tの部分だけをあとから変更できるため、同じ型定義を複数の型で再利用できます。

  • 型をあとから決められる
  • 同じ型定義や処理を複数の型で再利用できる
  • anyを使用せずに型情報を維持できる

ここからは、このUser型を中心に、実際のコードを少しずつ変更しながら確認していきましょう。

ジェネリクスを型定義に対して使う

まずは、型定義にジェネリクスを使用する方法を見てみましょう。

次のコードがあるとします。

const userOne: any = {
  id: "U001",
  name: "太郎",
};

現在はanyを使用しているため、userOneの型チェックが十分に行われません。

そこで、まずは普通の型定義を作成します。

type User = {
  id: string;
  name: string;
};

const userOne: User = {
  id: "U001",
  name: "太郎",
};

これで、idとnameにはstring型の値が入ることが分かります。

次に、もう1つユーザーを追加してみましょう。

type User = {
  id: string;
  name: string;
};

const userOne: User = {
  id: "U001",
  name: "太郎",
};

const userTwo: User = {
  id: 2002,
  name: "次郎",
};

このコードでは、userTwoのidでエラーになります。

Userでは、id: string;と定義しているため、number型の2002を代入できないからです。

では、userOneとuserTwoの両方で使用できる型にしてみましょう。

ここでジェネリクスを使用します。

type User<T> = {
  id: T;
  name: string;
};

これまでstringだったidの型をTに変更しました。

Tは、この時点では具体的な型が決まっていません。

実際にUserを使用するときに型を指定します。

文字列のIDを扱うuserOneでは、次のようにstringを指定します。

const userOne: User<string> = {
  id: "U001",
  name: "太郎",
};

この場合は、T = stringとなるため、idはstring型になります。

数値のIDを扱うuserTwoでは、次のようにnumberを指定します。

const userTwo: User<number> = {
  id: 2002,
  name: "次郎",
};

この場合は、T = numberとなるため、idはnumber型になります。

コード全体は次のようになります。

type User<T> = {
  id: T;
  name: string;
};

const userOne: User<string> = {
  id: "U001",
  name: "太郎",
};

const userTwo: User<number> = {
  id: 2002,
  name: "次郎",
};

このように、ジェネリクスを使用すると、Userという型定義を1つ用意するだけで、idに異なる型を使用できます。

これが、型定義に対して使用するジェネリクスの基本です。

関数にジェネリクスを使う

次は、関数にジェネリクスを使用する方法を見てみましょう。

関数では、主に次の2つの型を考えます。

  • 引数の型
  • 戻り値の型

例えば、渡されたIDをそのまま返す関数を考えてみましょう。「引数の型」と「戻り値の型」を明示すると、次のようになります。

function getId(id: string): string {
  return id;
}

この場合、id: stringが引数の型で、): stringが戻り値の型です。

TypeScriptでは戻り値の型は、推論できることも多いため、必ず明示する必要があるわけではありませんが、今回はジェネリクスの動きを分かりやすくするため、戻り値の型も記述して説明します。

この関数に文字列のIDを渡します。

console.log(getId("U001"));

// 実行結果
// U001

"U001"がidに入り、そのままreturnされるため、"U001"が返されます。

次に、数値のIDを渡してみます。

getId(2002);

このコードはエラーになります。

引数idが、id: stringと定義されているため、number型の2002を渡すことができないからです。

そこで、文字列と数値の両方のIDに対応できるように、ジェネリクスを使用してみましょう。

まず、「引数の型」と「戻り値の型」のstringをTに変更します。

function getId(id: T): T {
  return id;
}

ただし、このままではTが何なのかTypeScriptには分かりません。

そのため、この関数でTという型パラメーターを使用することを宣言します。

function getId<T>(id: T): T {
  return id;
}

これで関数のジェネリクス化は完了です。

文字列のIDを扱う場合は、次のように使用できます。

console.log(getId<string>("U001"));

// 実行結果
// U001

この場合は、T = stringになります。

数値のIDを扱う場合は、次のように記述します。

console.log(getId<number>(2002));

// 実行結果
// 2002

この場合は、T = numberになります。

同じgetId関数を使用しながら、文字列のIDでも数値のIDでも扱えるようになりました。

型引数は省略できる場合がある

関数のジェネリクスでは、毎回必ず<string>や<number>を指定する必要はありません。

例えば、次のようにも記述できます。

const idOne = getId("U001");
const idTwo = getId(2002);

TypeScriptは、渡された値から型を推論できます。

そのため、TypeScriptが型を推論できる場合には、型引数を省略できます。

一方、型を明示的に指定したい場合には、次のように記述します。

getId<string>("U001");
getId<number>(2002);

anyとの違い

さまざまな型を受け取りたいのであれば、anyでもよいのではないかと思うかもしれません。

例えば、次のように記述できます。

function getId(id: any): any {
  return id;
}

確かに、これでも文字列や数値のIDを渡すことができます。

しかし、anyを使用すると、TypeScriptによる型チェックが弱くなり、戻り値の型情報も失われます。

一方、ジェネリクスでは、

function getId<T>(id: T): T {
  return id;
}

とすることで、引数で受け取った型と戻り値の型の関係を維持できます。

つまり、ジェネリクスは単に「何でも受け取れるようにする仕組み」ではなく、型情報を保ったまま、複数の型に対応するための仕組みです。

型定義と関数の両方にジェネリクスを使う

次は、これまで使用してきたUser型とgetId関数を組み合わせてみましょう。

まず、User型には次のようにジェネリクスを使用しています。

type User<T> = {
  id: T;
  name: string;
};

const userOne: User<string> = {
  id: "U001",
  name: "太郎",
};

const userTwo: User<number> = {
  id: 2002,
  name: "次郎",
};

userOneではT = stringとなり、idはstring型になります。

一方、userTwoではT = numberとなり、idはnumber型になります。

次に、このUser型を受け取って、idを返すgetId関数を作ってみましょう。

現在、関数は次のようになっているとします。

function getId(user: any): any {
  return user.id;
}

現在は、引数userと戻り値の両方にanyを使用しています。

まず、引数userにUser型を指定してみます。

function getId(user: User): any {
  return user.id;
}

しかし、この書き方ではエラーになります。

Userはジェネリクスを使用しているため、User<string>やUser<number>のように、どの型を使用するのか指定する必要があるからです。

そこで、引数の型をUser<T>に変更します。

function getId(user: User<T>): any {
  return user.id;
}

これでUserの型をTで受け取れる形になりました。

ただし、このままではTがどこにも定義されていません。

そのため、関数名の後ろに<T>を追加して、この関数でTという型パラメーターを使用することを宣言します。

function getId<T>(user: User<T>): any {
  return user.id;
}

これで、getId関数でもジェネリクスを使用できるようになりました。

最後に、戻り値の型も変更します。

この関数が返しているのはuser.idです。

User<T>ではidの型がTなので、戻り値の型もTになります。

function getId<T>(user: User<T>): T {
  return user.id;
}

これで、型定義と関数の両方にジェネリクスを使用できました。

コード全体は次のようになります。

type User<T> = {
  id: T;
  name: string;
};

const userOne: User<string> = {
  id: "U001",
  name: "太郎",
};

const userTwo: User<number> = {
  id: 2002,
  name: "次郎",
};

function getId<T>(user: User<T>): T {
  return user.id;
}

console.log(getId(userOne));  // 明示的に「console.log(getId<string>(userOne));」と書いてもよい。
console.log(getId(userTwo));  // 明示的に「console.log(getId<number>(userTwo));」と書いてもよい。

// 実行結果
// U001
// 2002

userOneをgetIdに渡した場合、userOneはUser<string>型なので、Tはstringになります。

User<string>
     ↓
T = string
     ↓
戻り値もstring

そのため、getId(userOne)の戻り値もstring型になります。

一方、userTwoを渡した場合、userTwoはUser<number>型なので、Tはnumberになります。

User<number>
     ↓
T = number
     ↓
戻り値もnumber

となります。

このように、型定義と関数の両方にジェネリクスを使用すると、受け取ったユーザーのIDの型に応じて、戻り値の型も変化させることができます。

ジェネリクスを複数使う

ここまでは、User<T>のidという1つの型だけを変更してきました。

ジェネリクスでは、複数の型パラメーターを使用することもできます。

例えば、ユーザーのidとnameをそれぞれ変更できる型にしてみましょう。

まず、次のように定義します。

type User<T, K> = {
  id: T;
  name: K;
};

ここでは、

  • T → idの型
  • K → nameの型

を表します。

例えば、idが文字列、nameも文字列の場合は次のように記述します。

const userOne: User<string, string> = {
  id: "U001",
  name: "太郎",
};

この場合は、

T = string
K = string

となります。

一方、idが数値で、nameが文字列の場合は次のように記述できます。

const userTwo: User<number, string> = {
  id: 2002,
  name: "次郎",
};

この場合は、

T = number
K = string

となります。

関数でも同じように複数のジェネリクスを使用できます。

例えば、idとnameを受け取ってユーザー情報を作成する関数を考えてみましょう。

最初は、次のように文字列だけを受け取る関数があるとします。

function createUser(id: string, name: string): [string, string] {
  return [id, name];
}

この関数に数値のIDを渡すとエラーになります。

createUser(2002, "次郎");

そこで、idとnameの型をジェネリクスに変更します。

function createUser<T, K>(id: T, name: K): [T, K] {
  return [id, name];
}

例えば、次のように使用できます。

const user = createUser(2002, "次郎");

console.log(user);

// 実行結果
// [2002, "次郎"]

この場合は、

T = number
K = string

になります。

型引数を明示する場合は、次のようにも記述できます。

createUser<number, string>(2002, "次郎");

このように、ジェネリクスでは、<T, K>や、<T, K, U>のように、必要に応じて複数の型パラメーターを使用できます。

ジェネリクスに指定できる型を制限する

ジェネリクスを使用すると、さまざまな型を指定できます。

しかし、どのような型でも指定できると困る場合もあります。

例えば、ユーザーIDの文字数を取得する関数を考えてみましょう。

function getIdLength<T>(id: T): number {
  return id.length;
}

このコードはエラーになります。

Tにはnumberなど、lengthプロパティを持たない型が指定される可能性があるからです。

そこで、lengthプロパティを持つ型だけを指定できるようにします。

type HasLength = {
  length: number;
};

function getIdLength<T extends HasLength>(id: T): number {
  return id.length;
}

T extends HasLengthは、簡単にいうと、「Tにはlengthプロパティを持つ型だけを指定できます」という意味です。

文字列にはlengthプロパティがあるため使用できます。

console.log(getIdLength("U001"));

// 実行結果
// 4

一方、数値にはlengthプロパティがないため、次のコードはエラーになります。

getIdLength(2002);

extendsは少し発展的な内容なので、まずは基本的な<T>を理解してから覚えるとよいでしょう。

keyofとジェネリクスを組み合わせる

ジェネリクスは、keyofと組み合わせて使用することもあります。

ここでもUserを使用してみましょう。

const user = {
  id: "U001",
  name: "太郎",
};

このオブジェクトから、指定したプロパティの値を取得する関数を作ってみます。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

少し複雑に見えるので、TとK extends keyof Tがそれぞれ何を表しているのか順番に確認してみましょう。

まず、Tには第1引数objの型が入ります。

例えば、次のように実行したとします。

getProperty(user, "id");

第1引数にはuserを渡しているため、Tには次のような型が入ります。

T = {
  id: string;
  name: string;
}

次に、keyof Tを確認してみましょう。

keyofは、オブジェクト型が持っているプロパティ名を型として取得するために使用します。

今回のTが持っているプロパティはidとnameなので、keyof Tは、イメージとして次のようになります。

"id" | "name"

つまり、keyof Tは「idまたはname」という型になります。

そして、K extends keyof Tは、Kに指定できる型をkeyof Tの範囲に制限するという意味です。

今回のkeyof Tは、"id" | "name"なので、Kには"id"または"name"を指定できます。

整理すると、次のようになります。

T
↓
{
  id: string;
  name: string;
}

keyof T
↓
"id" | "name"

K extends keyof T
↓
Kには"id"または"name"だけを指定できる

そのため、次のコードはどちらも使用できます。

const id = getProperty(user, "id");
const name = getProperty(user, "name");

一方、userにはemailというプロパティが存在しません。

そのため、次のコードはエラーになります。

getProperty(user, "email");

K extends keyof Tによって、Kに指定できる値がTの持つプロパティ名に制限されているためです。

つまり、

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

では、

  • T:第1引数に渡したオブジェクトの型
  • keyof T:そのオブジェクトが持っているプロパティ名
  • K:第2引数に指定するプロパティ名
  • K extends keyof T:KをTが持っているプロパティ名だけに制限する

という関係になっています。

最初は、K extends keyof Tを「Kには、Tに存在するプロパティ名だけを指定できる」と考えると分かりやすいでしょう。

この書き方は少し複雑なので、最初からすべてを覚える必要はありません。

まずは<T>の基本を理解してから、extendsやkeyofとの組み合わせへ進むと分かりやすいでしょう。

型アサーションとは違う

TypeScriptでは、ジェネリクス以外でも<>を使用する書き方があります。

例えば、型アサーションには次のような書き方があります。

type User = {
  id: string;
  name: string;
};

const user = <User>{};

この<User>はジェネリクスではありません。

これは型アサーションの書き方の1つで、次のコードと同じ意味です。

const user = {} as User;

一方、次の<T>はジェネリクスで使用する型パラメーターです。

type User<T> = {
  id: T;
  name: string;
};

見た目はどちらも<>を使用していますが、意味は異なります。

書き方意味
<User>{}型アサーション
User<T>ジェネリクスの型パラメーターを定義
User<string>stringという型引数を指定
getId<string>("U001")関数のジェネリクスにstringという型引数を指定

なお、JSXを使用する.tsxファイルでは<User>{}という型アサーションの書き方は使用できないため、型アサーションではas Userの形式を使用するのが分かりやすいでしょう。

合わせて読みたい

「型アサーション」については、以下の記事で詳しく説明しています。興味のある方は下記のリンクからぜひチェックをしてみてください。

ジェネリクスはどんなときに使う?

ジェネリクスは、同じ処理やデータ構造を異なる型で使い回したい場合に便利です。

  • 同じ関数をstringやnumberなど複数の型で使用したい
  • Userのような同じ形のオブジェクトで、一部の型だけを変更したい
  • APIレスポンスなど、共通するデータ構造を型定義したい
  • 配列などの要素の型を維持したまま処理したい
  • 引数と戻り値の型の関係を維持したい

本記事のまとめ

この記事では「TypeScriptのジェネリクス」について、以下の内容を説明しました。

  • ジェネリクスとは、使用する型を最初から1つに固定せず、利用するときに指定したり推論させたりできる仕組み
  • Tなどは、あとから型を受け取る型パラメーター
  • User<string>のstringなどは、実際に指定する型引数
  • 型定義ではUser<T>のようにジェネリクスを使用できる
  • 関数ではfunction getId<T>(id: T): Tのようにジェネリクスを使用できる
  • 型定義と関数の両方にジェネリクスを使用できる
  • <T, K>のように複数の型パラメーターを使用できる
  • extendsを使用すると、指定できる型を制限できる
  • 型アサーションの<User>とジェネリクスの<T>は別のもの

ジェネリクスは、<T>や<K>などが登場するため、最初は難しく見えるかもしれません。

しかし、普通のUser型から始めて、固定されているid: stringをid: Tへ置き換えていくと、どのような仕組みなのか理解しやすくなります。

まずは、Tを「あとから型を入れる場所」と考え、User<string>ならTがstringになるという基本から覚えるとよいでしょう。

お読みいただきありがとうございました。

スポンサーリンク