TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - ジェネリクスとは何か

TypeScript TypeScript
スポンサーリンク

ゴール:「型を“あとから差し込める穴”として扱う感覚をつかむ」

ジェネリクス(Generics)は一言でいうと、

「型に“変数”を導入して、あとから具体的な型を差し込める仕組み」

です。

関数でいうと、(x: number) => numbernumber
「あとで決められるようにする」のがジェネリクス、というイメージです。

「型の再利用性」と「型安全」を同時に上げるための道具だと思ってください。

まずは“ジェネリックじゃない”関数から見る

number 専用の関数

例えば、配列の先頭要素を返す関数を考えます。

function firstNumber(arr: number[]): number | undefined {
  return arr[0];
}

const n = firstNumber([1, 2, 3]); // n は number | undefined
TypeScript

これは「number の配列専用」です。

string でも使いたくなったら、こう増えていきます。

function firstString(arr: string[]): string | undefined {
  return arr[0];
}
TypeScript

型ごとに関数を増やすのは、明らかにダルいですよね。

any を使うとどうなるか

「じゃあ any にしちゃえば?」とやるとこうなります。

function firstAny(arr: any[]): any {
  return arr[0];
}

const x = firstAny([1, 2, 3]);      // x: any
const y = firstAny(["a", "b", "c"]); // y: any
TypeScript

どんな配列でも受け取れるようにはなりましたが、
戻り値の型が any になってしまい、型安全性が消えます。

y.toUpperCase() と書いても、
コンパイラは何も教えてくれません。

ジェネリクス登場:「型の穴」を用意する

T という“型の変数”を導入する

ここでジェネリクスの出番です。

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = first([1, 2, 3]);        // n: number | undefined
const s = first(["a", "b", "c"]);  // s: string | undefined
TypeScript

<T> が「型の変数」です。

T は「なんでもいいけど、呼び出し時に決まる型」です。

first<number>([1, 2, 3]) と書いてもいいし、
TypeScript に推論させて first([1, 2, 3]) と書いても OK です。

ここで重要なのは、

「関数の中身は T という“抽象的な型”で書いておき、
呼び出し時に T に具体的な型(number や string)が入る」

という構造です。

any との決定的な違い

any とジェネリクスの違いをはっきりさせましょう。

function firstAny(arr: any[]): any {
  return arr[0];
}

function firstGeneric<T>(arr: T[]): T | undefined {
  return arr[0];
}
TypeScript

firstAny は「何を渡しても戻り値は any」。
型情報が消えます。

firstGeneric は「渡した配列の要素型を、そのまま戻り値に反映」します。

firstGeneric([1, 2, 3]) の戻り値は number | undefined
firstGeneric(["a", "b"]) の戻り値は string | undefined

というふうに、
「呼び出しごとに型がちゃんと変わる」のがジェネリクスの強みです。

ジェネリクスの基本構文と読み方

関数のジェネリクス

一番よく見る形はこれです。

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

const a = identity<number>(1);   // a: number
const b = identity("hello");     // b: string(推論される)
TypeScript

読み方としては、

identity は、型パラメータ T を受け取る関数。
引数 value は T 型で、戻り値も T 型。」

です。

つまり、

「T という“型の穴”を用意しておいて、呼び出し時にそこに具体的な型を入れる」

というイメージで読めれば OK です。

型パラメータは複数あってもいい

例えば、2つの配列を受け取ってタプルにする関数。

function pair<T, U>(a: T, b: U): [T, U] {
  return [a, b];
}

const p1 = pair(1, "a");      // [number, string]
const p2 = pair(true, { x: 1 }); // [boolean, { x: number }]
TypeScript

ここでは、

  • T は第1引数の型
  • U は第2引数の型
  • 戻り値は [T, U] というタプル型

になっています。

「型の変数が複数あるだけで、やっていることは同じ」です。

クラスでのジェネリクス

ジェネリックなコンテナクラス

例えば、「どんな型でも入れられる箱」をクラスで表現してみます。

class Box<T> {
  constructor(private value: T) {}

  getValue(): T {
    return this.value;
  }
}

const numberBox = new Box<number>(123);
const n = numberBox.getValue(); // n: number

const stringBox = new Box("hello"); // T は string と推論される
const s = stringBox.getValue();     // s: string
TypeScript

ここでも <T> が「型の穴」です。

Box<number> は「number を入れる箱」
Box<string> は「string を入れる箱」

というふうに、
同じクラス定義から、型の違うバリエーションをいくつも作れます。

重要なのは、

「クラスの中では T としか書いていないのに、
実際には number や string として振る舞う」

というところです。

ジェネリクスが“嬉しい”と感じる瞬間

「型ごとに同じ関数を量産しなくてよくなる」

さっきの firstNumber / firstString のように、
型ごとに関数を増やす必要がなくなります。

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
TypeScript

この 1 本で、

  • first<number>(number[])
  • first<string>(string[])
  • first<User>(User[])

など、いくらでも使い回せます。

「ロジックは同じで、型だけ違う」
という場面で、ジェネリクスは本領を発揮します。

「型安全を保ったまま汎用化できる」

any を使えば汎用化はできますが、
型安全性を失います。

ジェネリクスは、

「汎用性」と「型安全」を両立させるための仕組み

です。

例えば、配列を逆順にする関数。

function reverse<T>(arr: T[]): T[] {
  return [...arr].reverse();
}

const nums = reverse([1, 2, 3]);        // number[]
const strs = reverse(["a", "b", "c"]);  // string[]
TypeScript

戻り値の型がちゃんと「元の配列の要素型」に合わせて変わるので、
その後のコードでも型の恩恵を受け続けられます。

もう一歩:ジェネリクスに“制約”をつける

T に「なんでも入れていい」わけではない場合

例えば、「length プロパティを持つものだけ受け取りたい」関数。

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

getLength("hello");        // OK(string は length を持つ)
getLength([1, 2, 3]);      // OK(配列も length を持つ)
getLength({ length: 10 }); // OK
// getLength(123);         // エラー:number は length を持たない
TypeScript

T extends { length: number } が「型の制約」です。

ここでやっていることは、

「T はなんでもいいけど、少なくとも length: number を持っていてね」

という条件をつけている、ということです。

これにより、

  • 関数の中では value.length を安心して使える
  • 呼び出し側は「length を持たない型」を渡すとコンパイルエラーになる

という、気持ちいい状態になります。

まとめ:ジェネリクスとは何かを自分の言葉で説明すると

最後に、あなた自身の言葉でこう整理してみてください。

ジェネリクスとは、

「型に“変数”を導入して、あとから具体的な型を差し込める仕組み」。
<T> のような型パラメータを使うことで、
1つの関数・クラス・型定義を、いろいろな型に対して再利用できる。

any と違って、
「呼び出しごとに具体的な型が保たれる」ので、
汎用性と型安全を両立できる。

まずは、

function identity<T>(value: T): T { return value; }
class Box<T> { constructor(private v: T) {} getValue(): T { return this.v; } }
TypeScript

この 2 つを、自分の手で書いて、
T に number や string を差し込んで遊んでみてください。

そこで、

「T は“あとで決まる型の穴”なんだな」

と体で感じられたら、ジェネリクスの入口はもう突破できています。

タイトルとURLをコピーしました