TypeScriptのジェネリクス(Generics、総称型)について

ジェネリクス(Generics, 総称型)について書く。
動的型の言語から入ってきた人間は見た時に拒絶反応を起こしてしまうかもしれないやつ。

実際に利用されるまで型が確定しないケースなんかに使われます。

関数の使用例

// number型
const retArg = (arg: number): number => {
  return arg;
}

// string型
const retArg2 = (arg: string): string => {
  return arg;
}

このようにして同じ処理をしているのだけれどジェネリクスを使用することで一つの関数としてまとめられる。
上記の例を見て、引数にnumber型やstring型をstring | numberのようにして指定してあげればいいんじゃないの?って思う人もいるのではないだろうか?

ではこれがstringやnumber以外にも使用するまで何が入ってくるかわからない場合はどうするのか?という話になってくる。
また、一個一個型を指定していると型が冗長になってしまい可読性を下げる原因になります。

// 型を一個一個指定してすることで冗長化して読みづらいコード
const retArg = (arg: string | number | object | string[]): string | number | object | string[] => {
  return arg;
}

ジェネリクスはこれらを解決してくれます。

// Tは利用されるまで確定しない型のこと
function retArg<T>(arg: T): T { return arg; }

retArg<number>(1); // 使用時にnumber型が入ることを明示的に指定
retArg<string>("foo"); // 使用時にstring型が入ることを明示的に指定
retArg<string[]>(["foo", "hoge"]); // 使用時にstring型の配列が入ることを明示的に指定

// 引数から型が明示的にわかる場合は型推論ができるため省略が可能です
retArg("foo");

アロー演算子で定義したい場合。
コンパイラの問題でアロー演算子はextendsしてジェネリクスであることを示唆する必要がある。

// tsx時はこちらの書き方でOK
const retArg = <T>(arg: T) => arg;

// tsの場合
const retArg = <T extends {}>(arg: T) => arg;
const retArg = <T extends unknown>(arg: T) => arg;

https://stackoverflow.com/questions/32308370/what-is-the-syntax-for-typescript-arrow-functions-with-generics

クラスの使用例

次はクラスについての使用例。

クラスも同じ

class MyClass<T> {
  value: T;
  constructor(value: T) {
    this.value = value;
  }

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

// MyClassのvalueはstring型であると指定
const strObj = new MyClass<string>("foo");
console.log(strObj.getData());

// MyClassのvalueはnumber型であると指定
const numObj = new MyClass<number>(1);
console.log(numObj.getData());

例として、リクエストを送ってレスポンスによって返ってくる型が違う場合があるとする。
そんな場合でもジェネリクスを使用してレスポンスを明示的に示すことが可能。

以下のようなレスポンスクラスがあったとする。
dataにはAPIによってレスポンスが違うのでジェネリクスを使用する。

export class HttpResponse<T> {
  readonly data: T;
  readonly statusCode: number;

  constructor(data: T, statusCode: number) {
    this.data = data;
    this.statusCode = statusCode;
  }
}

API①が返す値が以下だとすれば

{
    data: 1,
    status: 200
}

dataはnumber型であるので

async request(params: Params): Promise<HttpResponse<number>> {
    const res = await axios.get({ data: params });
    const response = new HttpResponse(res.data.data, res.data.status);
    return response;
}

返り値に注目してほしい
Promise<HttpResponse<number>>引数に型を渡せばdataの型Tはnumber型と判断される

同様にAPI②が返す値が以下であれば

{
    data: [{ errorType: 'fatal error' }],
    status: 400
}

Promise<HttpResponse<object[]>>のように書く

async request(params: Params): Promise<HttpResponse<object[]>> {
    const res = await axios.get({ data: params });
    const response = new HttpResponse(res.data.data, res.data.status);
    return response;
}

のように扱えばdataはオブジェクトを格納した連想配列とわかる

インターフェース

interfaceに対してもジェネリクスは有効です。

interface IObj<T, U> {
    key: T;
    value: U;
}
const obj: IObj<string, number> = { key: "foo", value: 2 };

extendsを使った制約をすることも可能です。

interface IGetAge {
  age: number;
}
const getAge = <T extends IGetAge>(arg: T): number => {
  return arg.age;
}
getAge({ age: 18 });

Tはextendsで指定したインタフェースを満たす型でなければならないという意味になります。
つまり引数にはage、number型のオブジェクトを含むものを入れてくださいという制約になる。

ジェネリクスの名前の付け方

変数名は、特に定められていないが、基本的に大文字1字で定義する。
一般的には以下のような変数名が用いられる。

E – Element(要素)
K – Key(キー)
V – Value(値)
T – Type(タイプ)
N – Number(数値)
S,U – 2,3番目

関連記事