今回の詳しい内容は以下の公式ドキュメントにまとまっているので、英語が読める人はそちらを読んだほうがいいかも
今回はよく使う表現だけかいつまんで説明する
前提知識
ジェネリクスの話をする前に、ジェネリクスとともによく使われる文法をいくつか紹介します
keyof
演算子
keyof演算子は、指定したObject型のメンバのキーをString LiteralのUnion Typeとして列挙する演算子
以下のような感じで動作する
type A = {
a: string,
b: number
}
type a = keyof A // => 'a' | 'b'
typeof
演算子
typeof 演算子は、JavaScriptのObjectから型を作り出すことができる演算子。
この演算子があることによって、わざわざ型を別定義する必要がなくなるので非常に便利。
動作するプログラムコードの世界(JavaScriptの世界)の情報を型の世界にもっていく事ができるのでよく使われる
以下のように動作する
const A = {
a: 'aaa',
b: 0
}
type a = typeof A // => { a: string, b: number }
Indexed Access Types
Indexed Access Typesは、ある型の指定したメンバの型を返す演算子
例えば、user
オブジェクトのage
というプロパティの型を取得したいときに使える
type Person = {
age: number
name: string
alive: boolean
};
type Age = Person["age"] // => number
以上を踏まえた上でジェネリクスを見ていきます。
最も単純なジェネリクス例
以下のような引数をただ返すだけの関数を考えます。
function getArg(arg){
return arg
}
この関数にどのような型をつけるのが適切でしょうか?
もしも以下のように引数に指定できる型が決まっているのであれば、それを指定すればいいです。
function getArg(arg: number): number{
return arg
}
しかし、もしかすると引数に指定できる型にstringとnumberの両方を指定したい。というような場合があるかもしれません
function getArg(arg: number | string): number | string{
return arg
}
const result = getArg('テスト') // resultはstring | number型になる
この型付けは間違っているわけではありませんが、以下のような問題点があります
- 引数に
string
を指定した場合でも、返り値の型がstring | number
になってしまう
この問題は以下のようにanyを取れるようにしても同じです。引数になにを指定しようが返り値の型はanyになってしまいます
function getArg(arg: any): any{
return arg
}
const result = getArg('テスト') // resultはany型になる
この関数の実装から明らかなように、この関数は引数にstringを指定すればstringを返すし、numberを返せばnumberを返します。このように引数の型から返り値の型を絞り込みたいときに、ジェネリクスが活躍します。
以下がジェネリクスを使ったコードです
function getArg<T>(arg: T): T{
return arg
}
const result1 = getArg('テスト') // resultはstring型になる
const result2 = getArg(0) // resultはnumber型になる
この関数内で利用できる型変数T
というのを定義することができます。この定義は関数名<型変数名>(){}
のような形で、関数名と引数の()
の間に<>
で囲んで定義します
function getArg<T> // ⇐ここで型変数を定義
ここで定義した型変数Tは関数内で型付けする際に通常の型と同じように使用できます。ここでは引数argの型と返り値の型にともにTをつけました。
型変数Tの実際の型が決定するのは関数が呼び出されたときです。
TypeScriptのコンパイラは引数argに指定された型を見てそれをT型に割当て、返り値の型を推論します。これによって、引数と同じ型を返す関数を作ることができます。
get関数の型付け
以下のような関数 getValue
に型をつけることを考えます。
const a = {
name: 'a',
age: 10
}
function getValue(key){
return a[key]
}
この関数は、以下の性質を持っています。
- 引数keyはaのプロパティのみ指定できる
- 返り値の値は引数に指定したkeyのvalueになる
この性質を型を使って表すことを考えます。
まず、引数keyは、aのプロパティのみを指定できるようにしたいので、keyof
演算子とtypeof
演算子を使って以下のように表すことができます。
function getValue(key: keyof typeof a){
return a[key]
}
また、返り値はaのvalueの値になるので、以下のように表すことができます。
function getValue(key: keyof typeof a): (typeof a)[keyof typeof a]{
return a[key]
}
しかしこのままでは引数に指定したkeyのvalueを返り値の型に指定することができていません。
const a = {
name: 'a',
age: 10
}
function getValue(key: keyof typeof a): (typeof a)[keyof typeof a]{
return a[key]
}
const aa = getValue('name') // => string | numberと推論される
const bb = getValue('age') // => string | numberと推論される
しかしジェネリックを使えば引数に応じて返り値の型を変えることができます。
まず、以下のように引数の型を型変数Tとし、extends演算子を使ってTの型を縛ります。
ここでの T extends keyof typeof a
の意味は、T型は keyof typeof a
型と互換性のある型に制限するという意味です。
function getValue<T extends keyof typeof a>(key: T): (typeof a)[keyof typeof a]{
return a[key]
}
そしてここで指定した型変数Tを使って返り値の型を書き直します。
こうすることで、型変数Tを経由して引数の型と返り値の型を連動させることができます。
const a = {
name: 'aaa',
age: 0
}
function getValue<T extends keyof typeof a>(key: T): (typeof a)[T]{
return a[key]
}
const aa = getValue('name') // => stringと推論される
const bb = getValue('age') // => numberと推論される
const cc = getValue('ccc') // => 存在しないkeyなのでエラーになる
練習問題
以下のようなset関数に型付けをしてみましょう
const a = {
name: 'aaa',
age: 0
}
function setValue(key, value){
a[key] = value
}
答え
const a = {
name: 'aaa',
age: 0
}
function setValue<T extends keyof typeof a>(key: T,value:(typeof a)[T]){
a[key] = value
}