はじめに
毎週参加している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書いていきたいです。