😽

【TypeScript】Genericsの基礎

2022/06/30に公開

はじめに

TypeScript を学び始めた時の基本的なところの備忘録となります。

Generics とは?

型の決定を遅延できるもの。
基本的には以下のように記述する。

type Foo<T> = {
  value: T;
};

const foo: Foo<number> = {
  value: 0,
};

Generics で使用される T や K などの由来

Generics で使用される T や K などの由来は以下の通り。

  • T:Type
  • K:Key
  • U:Unknown
  • E:Element
    以上の 4 つが慣例的によく使われるので覚えておくと良い。

Generics の良さ

後から使いまわしたりする際に、型を自由に定義することができる。

type Foo<T> = {
  value: T;
};

const foo1: Foo<number> = {
  value: 0,
};
const foo2: Foo<string> = {
  value: "",
};
const foo3: Foo<number[]> = {
  value: [1, 2, 3],
};

Generics じゃなかったら value を定義し直さなければならなくなる。

Generics のユースケース

以下の例のように後から Japanese や American の型が決定させたい場合に使われる。

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

type Japanese = User<"東京都" | "大阪府">;
type American = User<"CA" | "NY">;

const user1: Japanese = {
  name: "香川",
  state: "東京都",
};

const user2: American = {
  name: "taylor",
  state: "CA",
};

Generics の初期値

<T = string>とすることで、初期値が string となる。
そのため、foo1 で Foo<string>としなくても、初期値は string だと暗黙的に分かっているので問題ない。

type Foo<T = string> = {
  value: T;
};

const foo1: Foo = {
  value: "",
};
const foo2: Foo<number> = {
  value: 111,
};

Generics の extends による型制約

Generics の型引数に制約を加えたい時に extends による型制約を用いる。
以下のコードだと、extends で string を指定しているので、string に互換性がない number は
エラーとなる。

type Foo<T extends string> = {
  value: T;
};

const foo1: Foo<string> = {
  value: "",
};
const foo2: Foo<"abc"> = {
  value: "abc",
};
const foo3: Foo<number> = {
  // 型 'number' は制約 'string' を満たしていません。
  value: 123,
};

Generics の初期値と extends による型制約の組み合わせ

シンプルに = string と繋げて記述すれば OK。

type Foo<T extends string = string> = {
  value: T;
};

const foo1: Foo = {
  value: "",
};
const foo2: Foo<"abc"> = {
  value: "abc",
};

関数 Generics の構文

以下のように記述する。

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

関数 Generics の使い方

以下のように記述する。

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

const foo1 = foo<number[]>([1, 2]);

アロー関数での記述方法

以下のように引数の()の前に Generics を記述する。

const foo = <T>(arg: T) => {
  return { value: arg };
};

const foo1 = foo<number[]>([1, 2]);

暗黙的な型解決について

関数の Generics は暗黙的に型が解決される仕組みがある。
以下のように<string>のようにしなくても引数の型を推論して、暗黙的に型を決定してくれる。

const foo = <T>(arg: T) => {
  return { value: arg };
};

const foo1 = foo("");
// value: string;
const foo2 = foo(1);
// value: number;
const foo3 = foo(false);
// value: number;

このおかげで関数で Generics を使用したとしても、使う側が毎回型指定をしなくて良くなるので、
非常に便利な機能。

どういう時に型引数を使うのか

Nullable かもしれない場合

Nullable かもしれない場合に以下のように型引数に null を入れて対応する。
※Nullable:Null になりうる値

const foo = <T>(arg: T) => {
  return { value: arg };
};

const foo1 = foo<string | null>("");
// value: string | null;

Nullable に限らず Union Types 等で後から引数に何が入ってくるかわからない場合に、
型引数を使用することが多い。

関数 Generics の extends による型制約

関数 Generics の型引数に制約を加えたい時に extends による型制約を用いる。

const foo = <T extends string>(arg: T) => {
  return { value: arg };
};

const foo1 = foo<string>("");
const foo2 = foo(1); // 型 'number' の引数を型 'string' のパラメーターに割り当てることはできません。

関数 Generics の extends による型制約は重要なもので、しっかりと理解しておく。

なぜ関数 Generics の extends による型制約は重要なのか

arg はある程度型を特定させるために必要で、もし extends による型制約が無かったら、
arg が中でどのような型になっているかが分からなくなってしまう。

const foo = <T>(arg: T) => {
  // argが中でどのような型になっているかが分からない

  return { value: arg };
};

そのため、型制約がないと unknown として扱われてしまい、メソッド等を呼び出せなくなってしまう。

const foo = <T>(arg: T) => {

  arg.
  // メソッドにアクセス出来ない

  return { value: arg };
}

型制約があると

const foo = <T extends string>(arg: T) => {
  arg.toUpperCase();
  // メソッドにアクセス出来る

  return { value: arg };
};

関数 Generics の extends による型の絞り込み

Union Types で string または number 等の場合、以下のように上手く扱えなくなるケースが出てくる。

const foo = <T extends string | number>(arg: T) => {
  arg.toUpperCase;
  // プロパティ 'toUpperCase' は型 'string | number' に存在しません。
  // プロパティ 'toUpperCase' は型 'number' に存在しません。

  return { value: arg };
};

そういった時に型の絞り込みを行い対応する。

const foo = <T extends string | number>(arg: T) => {
  if (typeof arg === "string") {
    return { value: arg.toUpperCase() };
  }

  return { value: arg.toFixed() };
};

型引数が Union Types の場合でも型の絞り込みを使うことによって、
関数内でうまく扱える。

ネストされたオブジェクトになっているなど、複雑な Union Types の場合でも
ユーザー定義の Type Guard を使うことによって型解決できる。

Generics の型引数が複数あるパターン

以下のようにカンマ区切りで記述する。

const foo = <T, K, U>(foo: T, bar: K, baz: U) => {
  return {};
};

また、以下のように型引数それぞれに初期値や extends の制約を付け加えることも出来る。

const foo = <T extends string, K extends number, U = boolean>(
  foo: T,
  bar: K,
  baz: U
) => {
  return {};
};

Loolup Types

以下のように T[K]の構文で記述する。

const foo = <T extends string, K extends number, U = boolean>(
  foo: T,
  bar: K,
  baz: U
) => {
  return {};
};

type Obj = {
  a: number;
  b: boolean;
};

type Foo = Obj["a"];

Generics と Loolup Types が合わさったパターン

第一引数の T は第二引数で用いることができる。
extends と keyof を用いて、オブジェクト T の key を K に制約している。
それにより、Loolup Types の obj[key]でエラーが出なくなる。

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

const obj = {
  foo: 1,
  bar: 2,
  baz: 3,
};

const hoge = getProperty(obj, "bar");
// const hoge: number

また、setProperty また、の例として以下のように記述する。
第三引数に value: T[K]とすることで、value は number 型となる。

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

const setProperty = <T, K extends keyof T>(obj: T, key: K, value: T[K]) => {
  obj[key] = value;
};

const obj = {
  foo: 1,
  bar: 2,
  baz: 3,
};

const hoge = getProperty(obj, "bar");

setProperty(obj, "bar", 100);

JavaScript で用いられる Generics

map 関数

map 関数の場合、以下のように型推論が上手く動いている。
map 自体が Generics になっていて、暗黙的な型解決でうまく推論してくれている。

const foo1 = [1, 2, 3].map((v) => v);
// const foo1: number[]

const foo2 = [1, 2, 3].map((v) => v.toString());
// const foo2: string[]
GitHubで編集を提案

Discussion