🪄

TypeScriptのジェネリクス型を知ろう

に公開

概要

別記事を書くにあたりジェネリクス型を理解しようの会

ジェネリクス型とは

引数の型を利用し表現することで安全性を保ちながら汎用性を高める型。
参考: https://www.typescriptlang.org/docs/handbook/2/generics.html#handbook-content

例えば下記の処理があるとする。

const hoge = 'ほげ'
const fuga = 1

const hogeArray = format(hoge)
const fugaArray = format(fuga)

function format(params) {
  return [params]
}

format()の引数と返り値に型指定をしたい場合に

function format(params: any): any[] {
  return [params]
}
function format(params: string | number): (string | number)[] {
  return [params]
}

みたいな書き方ができるが、anyだと何でもありだしstring|numberだと使用する度に増やしていかなきゃいけないし引数stringで返り値number[]でもありになってるし...みたいな感じになってしまう。

そこで、ジェネリクス型を使って

function format<T>(params: T): T[] {
  return [params]
}

と書いて安全性と汎用性の良いとこどりをしていく。
<T> を書くことによって変数を明示してあげる必要がある。

こんな感じで複数書くことも可能。

function format<T, K>(params: T, key: K): { key: K; params: T } {
  return { key, params };
}

ちょっと発展編

使い方は前述の通りだが、使用するときに以下のように extends とか typeof とか keyof とか出てきて混乱するので見てみる。

function formatWithConstraint<T extends string | number, K extends string>(
  params: T,
  key: K
): { key: K; params: T } {
  return { key, params };
}

function formatWithTypeof<T extends typeof myObject, K>(
  params: T,
  key: K
): { key: K; params: T } {
  return { key, params };
}

function formatWithKeyof<T, K extends keyof T>(
  params: T,
  key: K
): { key: K; value: T[K] } {
  return { key, value: params[key] };
}

extends

extendsの説明はこちらを参照。
今回は型の設定時の方を利用している。
引数の型であるT、Kに対してある程度の制限を設けることで安全性を強化している。

function formatWithConstraint<T extends string | number, K extends string>(
  params: T,
  key: K
): { key: K; params: T } {
  return { key, params };
}

formatWithConstraint("ほげ", "ほげkey"); // ✅Tがstring型、Kがstring型なのでOK
formatWithConstraint(2025, "ほげkey"); // ✅Tがnumber型、Kがstring型なのでOK
formatWithConstraint([1, 2, 3], "ほげkey"); // 🚫Tがstring型かnumber型以外なのでNG
formatWithConstraint(2025, 0); // 🚫Kがstring型以外なのでNG

typeof

typeofの説明はこちらを参照。
すでに定義されている値の型を基準に指定している。

const myObject = { name: "ほげ太郎", age: 30 };
const myObject2 = { name: "ほげ太郎", message: "こんにちは、ほげ太郎です" };
const myObject3 = { name: "ほげ太郎", age: 30, message: "こんにちは、ほげ太郎です" };
function formatWithTypeof<T extends typeof myObject, K>(
  params: T,
  key: K
): { key: K; params: T } {
  return { key, params };
}

formatWithTypeof(myObject, "key"); // ✅TがmyObjectの型なのでOK
formatWithTypeof(myObject2, "key"); // 🚫TがmyObjectの型じゃないのでNG(ageプロパティが抜けている)
formatWithTypeof(myObject3, "key"); // ✅TがmyObjectの型を拡張している(messageが追加されている)ものなのでOK

keyof

keyofの説明はこちらを参照。
型Tのキーとなっているものだけを許容するようにしている。

const myObject = { name: "ほげ太郎", age: 30 };

function formatWithKeyof<T, K extends keyof T>(
  params: T,
  key: K
): { key: K; value: T[K] } {
  return { key, value: params[key] };
}

formatWithKeyof(myObject, "name"); // ✅KがmyObjectのkeyofであるリテラル型 "name" | "age" に当てはまっているのでOK
formatWithKeyof(myObject, "key"); // 🚫KがmyObjectのkeyofであるリテラル型 "name" | "age" に当てはまっていないのでNG
formatWithKeyof("ほげ", "key"); // 🚫string型のkeyofにはstrig型は入ってないのでKにstring型は入れられずNG

Discussion