LoginSignup
6
9

More than 1 year has passed since last update.

TypeScriptのGenerics(ジェネリクス)型をまとめる

Last updated at Posted at 2022-02-12

はじめに

毎週参加しているTypeScript勉強会で、公式ドキュメントを読み解いたのでまとめます。

本記事の対象は、関数におけるGenericsのみになります。

Genericsとは?

例えば、配列の最初の要素を返す関数があるとする。

function firstElement(arr: any[]) {
  return arr[0];
}

上記は動作はするが常にanyを返してしまう。
こんな時に活躍するのがGenericsである。

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

実行時に<Type>という型の情報を引数のような形でもらい、引数や返り値のTypeに割り当てることができる。

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

console.log(firstElement<string>(['hoge', 'fuga']))
// output: "hoge"

console.log(typeof firstElement<string>(['hoge', 'fuga']))
// output: "string"

// <string>は省略可能(TypeScriptが推論してくれる)
firstElement(['hoge', 'fuga'])

上記の例では、実行時に<string>を渡すことで、firstElementの引数及び返り値がstringに制約される。

まとめるとGenericsとは、

  • 型の情報を引数のような形<>でやり取りできる
  • 定義時にはどんな型になるか分からない処理で、実行時に型を制約したい場合に使う
  • Genericsを使うことで、処理内の2つ以上の場所で使用することにより、例えば関数の引数と返り値のリンクを作成できる

Generics関数を良い感じに書く方法(ガイドライン)

※全て公式に載っている内容です。

extendsによる型制約に注意する

また配列の最初の要素を返す関数を例にする

function firstElement1<Type>(arr: Type[]) {
  return arr[0];
}

// 悪い例
function firstElement2<Type extends any[]>(arr: Type) {
  return arr[0];
}
 
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);

本来extendsを使うことでGenericsをより制限できるのだが、上記の例ではextendsを使用していないfirstElement1()numberを返しているのに対し、extendsを使用しているfirstElement2()anyを返しているので、良くないextendsの使い方である。

これは「TypeScriptはextendsによる制約をしている場合、実行時に型を解決するのではなく、実行前にextendsによる制約を使用して型解決を行なっているから」である。

つまり、extendsを使用する場合は、型を緩めるような使い方(上記のような<Type extends any[]>)はすべきではない。

Generics内でextendsを書く場合は、型をより制限するような形で書く

参考:
extends(良い使い方はこちらを参考に)
Push Type Parameters Down(引用元)

Genericsで定義する型パラメータはできるだけ少なく

function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
  return arr.filter(func);
}
 
function filter2<Type, Func extends (arg: Type) => boolean>(
  arr: Type[],
  func: Func
): Type[] {
  return arr.filter(func);
}

<>内を見ると、上は<Type>のみなのに対し、下は<Type, Func extends (arg: Type) => boolean>と記述が多い。
これがどういうことかと言うと、「呼び出し元から無駄なコードが発生する」「可読性が落ちる」「推論の精度が落ちる」などの問題が発生してしまう。

Genericsで定義する<>内は、できるだけシンプルに書く

参考:
Use Fewer Type Parameters(引用元)

Genericsで定義する型パラメータは、処理内で2回以上使用する場合のみ使う

// bad
function greet<Str extends string>(s: Str) {
  console.log("Hello, " + s);
}
 
// good
function greet(s: string) {
  console.log("Hello, " + s);
}

Genericsは、複数の値の型を関連づけるものなので、処理内で1回しか登場しない型には使うべきではない。

型パラメータが1回しか登場しない場合はGenericsは使用しない

参考:
Type Parameters Should Appear Twice(引用元)

最後に

公式には、Genericsは「こう書くべき」ではなく「こう書いてはいけない」というアンチパターンが多く紹介されていました。
普通に書いている分には、そう書かないよな..?というものもありましたが、これらのルールをきちんと守ってGenerics書いていきたいです。

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9