🐡

ジェネリクス型の理解を深めよう!

2023/11/24に公開

typescript 初学者の最初の難関ともいえるジェネリクス型
type を調べる際コードジャンプしたらTだのUだのKだか
よくわからない大文字をよく見かけると思います!
そんなよくわからない大文字達の意味と使い方を
なるべくわかりやすく解説していきます!

ジェネリクス型とは

ジェネリクス型とは型の決定を遅延できるものです。
ジェネリクス型を用いることにより後から
型を決定することができるので、
いろんなケースで対応することができるようになります。

実際にコードを書いてみよう

実際にコードを書いて解説していきます!

type Foo = {
  data: string;
};

Foo という関数の型の中に引数で data があるとします。
このデータの型を後から決定したい、
今は string だけど後から number にしたい
という場合にジェネリクス型の登場です。

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

このように書くことで data を後から決定
させることができます!
呼び出し方としては

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

const foo1: Foo<string> = {
  data: "foo",
};

const foo2: Foo<boolean> = {
  data: true,
};

const foo3: Foo<number[]> = {
  data: [1, 2, 3],
};

関数の型の後ろに<>を使います。
その中に文字列を使いたいなら string,
数字を使いたい場合は number を入力します
もちろん真偽値や null を指定することができます。

このようにジェネリクス型を指定することで
同じ型関数でも簡潔に型を指定することができます。

ついに出てきた大文字の T についても
解説していきます。

T とか U とかについて

結論からお伝えすると、
このTはType(タイプ)の略です!
なんと T とか K や U などは英語の大文字1文字目だったんです!
なーんだって感じですよね笑

一般に使用されている型パラメーター名を紹介します。

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

ジェネリクス型の実際の使い方

今回は User という型関数を用いて
ジェネリクス型を使ってみます。

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

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

const user1: Japanese = {
  name: "田中",
  state: "東京",
};

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

まず User という型に名前と state として
ジェネリクス型であとから指定したい型を
引数Tとして指定しています。

さらに関数の型で Japanese と American を準備し
先程の型 User の引数にそれぞれ値を入力します。

これを行うことにより新しい関数 user1,user2 を
作成した際に型関数を Japanese 又は American と
入力することにより,
state を入力する際に値の候補を出してくれます。
便利ですね。

結果的にユーザーの型定義を
ジェネリクスを用いることで
型を後から指定することができました。

ジェネリクスの初期値

ジェネリクスには初期値を設定することができます。
実際にコードを書きながら解説していきます。

結論から言うとジェネリクスの後ろに
= を書いて初期値を入力します。

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

// Fooの初期値はstringなので怒られない
const foo1: Foo = {
  value: "",
};

// 通常通りの使い方もできます。
const foo2: Foo<number> = {
  value: 111,
};

ジェネリクスの extends を使った型制約

ジェネリクスとはいえ、ある程度型を
制限したいよねって時に extends を使います

extends を使用することにより
型の制約を行うことができます。

実際にコードを書いていきます。

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

// ◯
const foo1: Foo<string> = {
  value: "",
};

// ◯
const foo2: Foo<"value"> = {
  value: "value",
};

// ✕
// 型'number' は制約'string'を満たしていません
// とエラーがでる。
const foo3: Foo<number> = {
  value: 111,
};

このように入力することで型の制約を
行うことができます。
今回の場合は<>の中に string または string リテラルタイプしか
使用することができないようになっています。

extends にはユニオン型を用いることができます。

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

// ◯
const foo1: Foo<string> = {
  value: "",
};

// ◯
const foo2: Foo<"value"> = {
  value: "value",
};

// ◯
const foo3: Foo<number> = {
  value: 111,
};

string または number なので先程こコードは
すべて怒られなくなります。

初期値と extends の組み合わせ

先程の extends に初期値を入れることができます。
やり方は簡単です。

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

const foo1: Foo = {
  value: "",
};

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

パッとみて分かりづらいとは思いますが、
extends による型制約で string 又は number であり
初期値が string となっております。

したがって value の返り値が string の場合
<string>を省略することができます。

関数のジェネリクス

関数でもジェネリクスを用いることができます。
例のごとくコードを書いて解説していきます。

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

const foo1 = foo<string>("bar");

関数名の横にジェネリクスを記入します。
さらに引数にもジェネリクスを使用することで
関数として使用することができます。

使用する際は関数を呼び出し、
その関数自体に型を指定することにより
使用することができます。

関数のジェネリクスは
自動的に型を推論してくれます。
先程のコードで見てみましょう

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

// string型
const foo1 = foo("bar");

// number型
const foo2 = foo(2);

//boolean型
const foo3 = foo(true);

このように書いてもエラーが出ず
自動的に型を付与してくれます。
とても便利です。

null になる可能性がある値の場合は

const foo1 = foo<string | null>("bar");

このようにユニオン型で定義してあげれば、
文字列ではなくデータがない場合でもエラーを
吐かれることはありません。

関数のジェネリクスの extends による型制約

type エイリアスとやり方は同じです。
実際にコードを書いて解説していきます。

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

const foo1 = foo("bar");

ジェネリクスの後ろに extends と書き、
その後ろに指定したい型を記入します。

型制約するメリットについて

ジェネリクスの型を制約することで
様々なメリットがあります。

1.パッと見てジェネリクスの型が何が必要かわかる。

これはわかりやすいですね。
先程のコードで見てみましょう

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

foo の引数である arg は string なんだな、
というのが一目でわかるはずです。
これが1つ目のメリットです。

2.プロパティの候補を出してくれる

extends による型制約を行うことによって
プロパティの候補を出してくれるようになります。

今回のサンプルコードでは return で
{value: arg}として返していますが、
実際にはもっと色々なコードを書くことになると思います。
その際にプロパティの候補が出ないと
色々と不都合が起きると思いますので

ジェネリクスでの関数で型制約を行うのは
重要ということです。

extends による型の絞り込み

先程プロパティの候補を出してくれると言いましたが
extends による型制約がユニオン型の場合どうなるか
見てみましょう

function foo<T extends string | number>(arg: T) {
  return { value: arg };
}

この場合 string または number と、どっちになるか
まだわからない状態なので
プロパティの候補をだしてくれません。
また、string のプロパティを入力しても
number の可能性があるので、エラーを吐かれてしまいます。

じゃあこの場合どうすればいいのかというと
型の絞り込みを行うことで
プロパティの候補をだしてくれるようになります。

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

if 文を用いることで型の絞り込みを行うことが出来ました。
value の arg に string のプロパティを付与することができます、
最後のreturn { value: arg };の部分は
必然的に number になりますので、
arg に number のプロパティを付与することができます。

ジェネリクスの型引数が複数ある場合

ジェネリクスの型引数が複数ある場合を見てみましょう。
サンプルコードを書いていきます。

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

ジェネリクスの型が引数が複数ある場合
このように書きます。

それぞれの引数に extends や初期値を入力することができます。

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

一点注意として初期値を入力すると省略することができると言いましたが
extends を使う場合第一引数を初期値をいれて、
第二引数を extends をするとエラーが発生します。

// 必須の型パラメーターの後に、オプションの型
// パラメーターを続けることができません
const foo = <T = string, S extends number, U = string>(
  foo: T,
  bar: S,
  baz: U
) => {
  return {};
};

S というのは number の互換性のある型をいれないといけないのですが、
こちらの T の方は初期値で string が入っているので省略できてしまうので
こういう書き方はダメですと怒られてしまいます。

Lookup Types と組み合わせる

ジェネリクスは Lookup Types と組み合わせるケースがあります
コードで見てみましょう

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

const user = {
  id: 1,
  name: "Jun",
  age: 32,
};

// 第二引数でuserのkeyの候補をだしてくれる
// keyの候補とは、id,name,ageのこと// getUserは

// barはnumber型になります。
const bar = getUser(user, "id");

このコードでは関数 getUser で渡されたオブジェクトtypeの中から
指定されたキーkeyの値を返しています。

user のオブジェクトでは id,name,age というキーを持った
プロパティをもたせており、

最後に getUser 関数で第一引数にオブジェクトを指定し、第二引数でキーを
指定することで
関数 bar の型を取り出しています。

応用してコードを拡張してみましょう。
値を変える setUser 関数を作成します。

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

const setUser = <T, K extends keyof T>(type: T, key: K, value: T[K]) => {
  // オブジェクトのキーを変更
  type[key] = value;
};

const user = {
  id: 1,
  name: "Jun",
  age: 32,
};

const bar = getUser(user, "id");

// 第三引数で値を変更することによりオブジェクトの
// 値が変更されます
// 今回はageを指定しているのでstring型だとエラーがでます。
setUser(user, "age", 18);

第3引数として value を作成しました。
型としては T[K]、オブジェクトのキーを型として付与します。
type[key]が value の値に変更するようにし、

最後に setUser で Jun を 18 歳に若返らせています。

まとめ

TypeScript のジェネリクスは本当に難しいと感じました!
特に最後の Lookup types との組み合わせは
頭がこんがらがると思うので
1つづつ紐解いていきましょう。

最後までお読みいただきありがとうございました。

Discussion