目標
以下のコードが理解できるようになることが本記事の目標です!
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
type Human = {
name: string;
age: number;
}
const tanaka: Human = {
name: "tanaka",
age: 26
}
const tanakaName = get(tanaka, "name"); // tanakaNameはstring型
console.log(tanakaName); // 出力: "tanaka"
const tanakaAge = get(tanaka, "age"); // tanakaAgeはnumber型
console.log(tanakaAge); // 出力: 26
動作解説
定義されているget
関数がやっていることは、引数として渡されたobj[key]
を返すだけです。
tanaka
オブジェクトのname
プロパティにアクセスすれば、”tanaka”
が取得できますし、age
プロパティにアクセスすれば26
が取得できます。
型を省いてJavaScriptで実装すると以下のようになります。とてもシンプルですね。
function get(obj, key) {
return obj[key];
}
const tanaka = {
name: "tanaka",
age: 26
}
const tanakaName = get(tanaka, "name");
const tanakaAge = get(tanaka, "age");
ただ、これをTypeScriptで実装しようとする時、いくつか問題点があります。
-
get
関数の引数に決まった型がない(第1引数のオブジェクトはHuman
型であるとは限らない) - 返り値の型が
string
とnumber
で異なる -
get
関数の第2引数はtanaka
のプロパティだけを指定できるようにしたい
ジェネリクス
ジェネリクスを使うと、型は関数を呼び出した時に決める、ということはできるようになります。これを使えば1つ目の問題が解決しますね。
簡単な例を見てみましょう。
function test<T>(arg: T): T {
return arg;
}
test<number>(1); // number型の値、1が返ってくる
test<string>("文字列"); // string型の値、"文字列"が返ってくる
抽象的な型引数T
を関数に与え、実際に利用されるまで型が確定しない関数を作成することによって、後で型を決められるようにしています。抽象的な型引数は複数渡すことができます。
ここで最初のコードを見てみましょう。
// 一部簡略化
function get<T, K>(obj: T, key: K): T[K] {
return obj[key];
}
get
関数にはT
とK
という抽象的な型引数が渡されています。T
とK
はget
関数呼び出し時に与えることにしている、ということです。obj
の型は後から渡されるT
が、key
にはK
が型として渡されます。
ですが実際にget
関数を呼び出している部分を見てみると、T
やK
に該当する方が渡されていません。このような時は型推論によってT
とK
は決められます。get(tanaka, "name");
とした場合、実際には以下のような型に推論されます。
function get<Human, "name">(obj: Human, key: "name"): string
T
はHuman
型、K
は”name”
というリテラル型に推論されます。返り値であるT[K]
はstring
に推論されていますが、これを理解するにはlookup型を理解する必要があります。
lookup型
lookup型はT[K]
という構文を持つ型で、T
とK
は型です。T
にはオブジェクト型、K
には文字列のリテラル型が用いられることが多いです。簡潔に書くと「T[K]
はT
が持つK
プロパティの型」ということです。
今回解読しようとしているコードでは、T
はHuman
というオブジェクト型、K
は”name”
という文字列のリテラル型が割り当てられています。Human
型を見てみると、name
プロパティの型はstring
になっています。よってT[K]
はstring
に推論されているというわけです。
type Human = {
name: string; // Human[name]のとき、T[K]はstring
age: number; // Human[age]のとき、T[K]はnumber
}
lookup型を使うことによって、get
関数の返り値の型が異なるという、2つ目の問題が解決されます。
keyof型
最後に3つ目の問題についてですが、これは型引数K
にくっついているK extends keyof T
という部分が解決してくれます。この構文はextends
とkeyof
という2種類の構文が混じっているため、一つずつ見ていきましょう。
まずはkeyof
についてです。keyof
型は、オブジェクト型からそのオブジェクトのプロパティ名の型を得る機能です。
type Test = {
id: number;
pass: string;
}
// "id" | "pass" という型と同義
type TestKeys = keyof Test;
let key: TestKeys = "id";
key = "pass";
key = "hoge" // これはエラー
Testkeys
型は、Test
型のプロパティ名だけが許されている型です。”id”
と”pass”
というリテラル型しか受け付けません。keyof型を使うとこのような型を簡単に作ることができます。
今回解読しようとしているコードでは、型引数T
にはHuman
型が割り当てられます。つまりkeyof T
は”name” | “age”
ということになります。
部分型
続いてextends
についてです。これは部分型という制約があることを表しています。部分型は2つの型の互換性を表すものです。
type Animal = {
age: number;
}
type Human = {
name: string;
age: number;
}
Animal
型とHuman
型がこのように定義されているとき、Human
型はAnimal
型の部分型であると言えます。Animal
を満たすオブジェクトがHuman
を満たすとは限りませんが、Human
を満たすオブジェクトはAnimal
を必ず満たします。同じage
プロパティをもち、その型も同じであるからです。このような関係にある型を部分型関係だと言えます。
部分型について理解いただけたところで、extends
について解説します。extends
は、型引数に対して何らかの部分型であるという制約をつけたい時に使われます。
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
get
関数の場合、型引数K
はtypeof T
の部分型でなければならないという制約が課されています。つまりK
は”name” | “age”
の部分型でなければならないということです。この制約があることによって、get
関数の第2引数はHuman
型のプロパティ名のリテラル型しか受け付けないようになっています。
おまけ:Kにextends keyof Tを指定しないと…
ジェネリクスの解説の時にget
関数の型は以下のように推論されると書きましたが、key
はstring
に推論されるのではないか?と思った方がいるかも知れません。
function get<Human, "name">(obj: Human, key: "name"): string
型引数 K
に対してextends keyof T
という制約がなければ、key
はstring
に推論されます。しかしK
に対してextends keyof T
を指定しない場合、「Type ‘K’ cannot be used to index type ‘T’
」というコンパイルエラーになります。get
関数の返り値の型T[K]
について、不都合が生じる可能性があるからです。T[K]
の意味からすると、T
は何らかのオブジェクト型で、K
はそのオブジェクト型が持つプロパティである必要があります。extends keyof T
がないとこれが保証できなくなるため、コンパイルエラーとなるのです。
まとめ
今回はTypeScriptに欠かせないジェネリクスや部分型にプラスして、lookup型やkeyof型についての解説も行いました。後者の2つを使いこなすには練習が必要ですね。
コード解説の部分で提示したJavaScriptのコードを、コンパイルエラーが出ないように型を付け足していくと、今回の内容が復習できるのではないかと思います。ぜひ挑戦してみてください。
本記事に誤りや誤字脱字がございましたら、コメント等でご指摘いただけるとありがたいです。
参考
ジェネリクス (generics) | TypeScript入門『サバイバルTypeScript』
【TypeScript】Generics(ジェネリックス)を理解する - Qiita
TypeScript の lookup 型をマスターしよう - コムテブログ