TypeScriptのジェネリクスとは?Tの意味と使い方を初心者向けに解説

TypeScriptのジェネリクスとは?初心者向けにの意味や使い方を分かりやすく解説
少年A
TypeScriptの <T> って何?ジェネリクスって難しそうで避けてたんだけど…
キノ
実は「型を引数みたいに受け取る仕組み」ってだけなんだ。まずはなぜ必要なのかから見てみよう
少年A
any じゃだめなの?
キノ
それがだめな理由が、ジェネリクスの出番そのものなんだ。順番にいくよ

TypeScriptのジェネリクスとは?型を引数として受け取る仕組み

TypeScript ジェネリクスとは、関数や型の定義時には型を決めず、使うときに型を渡せるようにする仕組みです。型を「引数」のように受け取るため、同じ処理を型ごとに書き直さずに、型安全に使い回せます。

TypeScript ジェネリクス(<T>)とは:identity<T>にstring・number・booleanの型を渡して使い回す図

関数の引数が「呼び出すときに値を渡す」ものなら、ジェネリクスは「呼び出すときに型を渡す」もの、とイメージすると分かりやすいです。

普通の引数型引数(ジェネリクス)
渡すもの値(42 など)型(number など)
書く場所( ) の中< > の中
例add(1, 2)identity<string>("a")
スポンサーリンク

ジェネリクスが必要な理由:型ごとに関数を書くと重複する

受け取った値をそのまま返すだけの関数を、型ごとに書くとこうなります。

function identityString(value: string): string {
  return value;
}
function identityNumber(value: number): number {
  return value;
}

処理は同じなのに、string用・number用と増えていきます。boolean や配列にも対応するたびに関数が増え、修正も全部に必要になります。

では引数と戻り値を any にすればよいかというと、それも問題があります。詳しくは後の「anyとの違い」で見ます。

スポンサーリンク

基本の書き方:関数名の後ろに <T> を付ける

ジェネリクスは、関数名の後ろに <T> を付けて型引数を宣言します。

function identity<T>(value: T): T {
  return value;
}

const a = identity<string>("hello"); // string
const b = identity<number>(42);      // number
  • <T>:型引数の宣言(Tは「Type」の頭文字で、慣習的によく使われる名前)
  • value: T:引数の型にTを使う
  • ): T:戻り値の型にもTを使う
  • identity<string>(...):呼び出し時に T = string を渡す

Tという名前に決まりはなく、<Type> や <Item> のように意味のある名前にしても動きます。慣習として1文字(T, U, K, V)がよく使われます。

型推論で <T> を省略できる

引数から型が決まる場合、呼び出し時の <string> は省略できます。TypeScriptが引数の値を見て、Tを自動で推論してくれるためです。

function identity<T>(value: T): T {
  return value;
}
const c = identity("hello"); // string と推論される
const d = identity(42);      // number と推論される

実務では、型推論に任せて省略するのが一般的です。推論がうまくいかないとき(引数がなく戻り値の型でしか決まらない場合など)だけ、<型> を明示します。

複数の型引数:<K, V> のようにカンマで並べる

型引数は複数宣言できます。ペアを作る関数なら、キーと値で別々の型を受け取れます。

function pair<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}
const p = pair("age", 30); // [string, number]

配列から先頭の要素を取り出す関数も、ジェネリクスで書くと要素の型がそのまま戻り値に引き継がれます。

function firstItem<T>(items: T[]): T | undefined {
  return items[0];
}
const n = firstItem([10, 20, 30]);   // number | undefined
const s = firstItem(["a", "b"]);     // string | undefined

extendsで型引数に制約をつける

型引数Tは「何でも入る」ため、そのままでは中身のプロパティにアクセスできません。たとえば length を使おうとするとエラーになります。

function getLength<T>(value: T): number {
  return value.length;
}

このエラーは、extends で「Tは少なくとも length を持つ型」という条件を付けると解消できます。これをジェネリクスの制約(generic constraints)と呼びます。

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}
getLength("hello");   // 5
getLength([1, 2, 3]); // 3
getLength(123);

制約を満たさない型(number など)を渡すと、コンパイル時にエラーになります。

// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

制約の公式な解説は、TypeScript公式ドキュメントの「Generic Constraints」にあります。

ジェネリックな型:type や interface にも使える

ジェネリクスは関数だけでなく、type や interface にも使えます。API レスポンスのように「形は同じで中身の型だけ違う」データを表すのに便利です。

type ApiResponse<T> = {
  data: T;
  status: number;
};
type User = { id: number; name: string };

const res: ApiResponse<User> = {
  data: { id: 1, name: "キノ" },
  status: 200,
};
const bad: ApiResponse = { data: 1, status: 200 };

ApiResponse<User> のように型引数を渡すと、data の型が User に決まります。型引数を渡さずに使うと、最後の行のようにエラーになります。

// Generic type 'ApiResponse' requires 1 type argument(s).

anyとジェネリクスの違い:型情報が残るかどうか

any は「型チェックをやめる」型で、ジェネリクスは「型を保ったまま使い回す」仕組みです。まず any で書いた場合を見ます。

function identityAny(value: any): any {
  return value;
}
const r = identityAny("hello");
r.toFixed(2); // コンパイルは通るが、実行時にエラーになる

戻り値が any になるため、string を渡したのに toFixed(数値用のメソッド)を呼んでもエディタは何も警告しません。ジェネリクスで書くとこうなります。

function identity<T>(value: T): T {
  return value;
}
const s = identity("hello");
s.toFixed(2);
// Property 'toFixed' does not exist on type '"hello"'.

ジェネリクスなら、渡した型が戻り値まで引き継がれるため、間違った使い方をコンパイル時に検出できます。

anyジェネリクス(<T>)
型チェック実質なしあり
戻り値の型any(情報が消える)渡した型がそのまま残る
エディタの補完効かない効く
間違いの検出実行時コンパイル時
向いている場面型が本当に不明なとき(原則避ける)型が違うだけで処理が同じとき

型が分からない値を扱いたい場合は any ではなく unknown を使う方法もあります。違いはこちらで解説しています。

実務で見かけるジェネリクス:Array・Promise・useState

自分でジェネリクスを書く機会が少なくても、標準の型やライブラリでは日常的に使われています。代表例を見ておくと、<T> の意味がつかみやすくなります。

Array<T> と Promise<T>

const nums: Array<number> = [1, 2, 3];
const names: string[] = ["a", "b"];

async function fetchNumber(): Promise<number> {
  return 1;
}

number[] は Array<number> の短い書き方です。Promise<number> は「あとで number が返ってくる」という意味で、API通信の戻り値の型として頻出です。

ReactのuseState<T>

Reactの useState も、ジェネリクスで状態の型を指定できます。初期値が null で後からデータが入る場合に使います。

import { useState } from "react";

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

export function Profile() {
  const [user, setUser] = useState<User | null>(null);
  const [count, setCount] = useState(0); // number と推論される
  return (
    <button onClick={() => setUser({ id: count, name: "キノ" })}>
      {user?.name} <span onClick={() => setCount(count + 1)}>+</span>
    </button>
  );
}

初期値が 0 のように型が決まる場合は、useState(0) と書けば number と推論されるため、型引数は不要です。

React側の型の付け方は、React公式ドキュメントにもまとまっています。

よくあるエラーと対処

1. Property ‘length’ does not exist on type ‘T’

Tに何も制約がないのに、プロパティにアクセスしたときのエラーです。extends で必要なプロパティを持つ型に制約すると解消します(前述)。

2. Generic type ‘ApiResponse’ requires 1 type argument(s)

ジェネリックな型を、型引数なしで使ったときのエラーです。ApiResponse<User> のように型引数を渡してください。

3. Argument of type ‘number’ is not assignable to parameter of type ‘{ length: number; }’

制約(extends)を満たさない型を渡したときのエラーです。渡している値の型が制約に合っているかを確認します。

4. .tsxファイルで <T> を使うとエラーになる

アロー関数のジェネリクス <T> は、.tsxではJSXのタグと見分けがつきません。<T,> のようにカンマを付けるか、<T extends unknown> と書くか、関数宣言で書きます。

const identity1 = <T,>(x: T) => x;
const identity2 = <T extends unknown>(x: T) => x;

よくある質問

Q. ジェネリクスのTは何の略ですか?
A. 一般にはTypeの頭文字です。名前に決まりはなく、<Item> のように好きな名前にできます。複数あるときはT, U, V や K, V(Key, Value)がよく使われます。

Q. ジェネリクスとanyはどう使い分けますか?
A. 渡した型を戻り値などに引き継ぎたいならジェネリクスを使います。anyは型チェックが外れるため、原則避けるのがおすすめです。

Q. 初心者はジェネリクスをいつ覚えるべきですか?
A. 最初は Array<T>、Promise<T>、useState<T> を「読める」ことを目標にすると十分です。自作するのは、同じ処理を型違いで何度も書きそうになったときで構いません。

Q. 型引数は省略できますか?
A. 引数から型が推論できる場合は省略できます。推論できない場合や、意図した型を明示したい場合に <型> を書きます。

まとめ:TypeScript ジェネリクスとは何かのおさらい

  • ジェネリクスは、型を引数のように受け取り、使うときに決める仕組み
  • <T> を関数名や型名の後ろに付けて宣言する。多くの場合は型推論で省略できる
  • extends で型引数に制約をつけると、プロパティにアクセスできる
  • anyと違い、渡した型が戻り値まで引き継がれ、間違いをコンパイル時に検出できる
  • Array<T>・Promise<T>・useState<T> を読めるようになるのが最初の目標

次のステップとして、型の基本や設定を固めておくと、ジェネリクスの理解も早まります。

少年A
<T> は「型を渡す引数」って考えればいいんだね。思ったよりシンプルかも
キノ
そう。まずは Array<T> や Promise<T> が読めれば十分。自分で書くのはその後で大丈夫だよ

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です