📑

TypeScriptのジェネリクスについて

に公開

ジェネリクスとは

ジェネリクスは型を安全に使いつつ使いまわせるようにする便利機能です。
型の安全性と汎用性のあるコードはトレードオフでしたがこれを解決するような概念になります。

どのような恩恵がある?

ジェネリクスの恩恵は先ほど述べたように型の安全性を担保したまま汎用的に使い回すことができるところです。

まずはよろしくない書き方から。
汎用的に使いたいので引数の型にany[]を使って全ての型の引数を許容するように書いてみます。

function getFirstItemBad(items: any[]): any {
  return items[0];
}

const numbers1 = [1, 2, 3];
const result1 = getFirstItemBad(numbers1);
// result1の型はanyなので、存在しないメソッドを呼んでもエラーにならない
result1.toUpperCase();

numberの配列が引数に渡り、最初の要素に対してtoUpperCase()を実行しようとします。result1の値は文字列ではないので当然エラーになります。

コンパイルをして実行結果を確認すると

result1.toUpperCase();
        ^

TypeError: result1.toUpperCase is not a function
    at Object.<anonymous>

コンパイルは失敗せずランタイムエラーとなりました。この書き方だとコンパイルする前の静的解析で型がおかしいことに気づけないので改善する必要があります。

ではジェネクリスを使って定義してみます

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

const strings = ["a", "b", "c"];
const resultStrings = getFirstItem<string>(strings);
console.log(resultStrings?.toUpperCase());
// 出力結果: A

const numbers = [1, 2, 3];
const resultNumbers = getFirstItem<number>(numbers);
console.log(resultNumbers?.toFixed());
// 出力結果: 1

このように使いたい変数の型がstringの時はgetFirstItem<string>()、numberの時はgetFirstItem<number>()のように型を変数のように扱えるのがジェネリクスの良いところです。これによって型安全性を保ちつつ汎用的なコードが書けます。

const numbers2 = [1, 2, 3];
const result2 = getFirstItem(numbers2);
// result2の型はnumber | undefinedなので、存在しないメソッドを呼ぶとエラーになる
result2.toUpperCase();

数字の配列のnumbers2を定義して、最初の要素を受け取りその値に対してtoUpperCase()を実行しようとするとエラーになります。ちゃんと静的解析でコンパイル前に型がおかしいと怒ってもらえました。

image.png

ちなみにgetFirstItem()を呼び出す際に

const result2 = getFirstItem<number>(numbers2);

のように呼び出さなくても引数(配列)の型に応じて型推論をして型をつけてくれますね。

どのような場面で使うのか

例えばAPIレスポンスを受け取る際に使うことができます。

interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  return response.json();
}

async function example() {
  const userResponse = await fetchData<User>("https://api.example.com/user/1");
  if (userResponse.success) {
    console.log(userResponse.data.name);
    console.log(userResponse.data.price);
  }

  const productResponse = await fetchData<Product>(
    "https://api.example.com/product/1"
  );
  if (productResponse.success) {
    console.log(productResponse.data.title);
    console.log(productResponse.data.email);
  }
}

APIを叩く処理は一緒でもresponseの中身が異なる時には型安全性を担保するためレスポンスごとに型と関数を定義してあげる必要がありますが、ジェネリクスを使うと一つの関数とそれぞれのresponseのinterfaceを用意してあげれば、上の例のように関数を使い回して汎用的に、そして型を安全に定義したコードを書くことができます。

Discussion