TypeScriptをのお作法を順番に学習していく中で、ジェネリクスが出てきたくらいから急に難易度が上がるとともに理解度が下がった気がしていました。
が、とある構図の関数の記法を理解したことによって関連記法の理解が一気に深まったという経験がありましたので、是非ともその記法を共有したいなと思い記事にしてみました。
対象
- TypeScript初学者ながら、一個ずつの型定義やtypeやinterfaceなどのお作法を何となく理解している方
- ジェネリクスの理解に苦戦している方
記法
早速ですが、共有したい記法は以下のとおりです。
関数式となっています。
const valueOfPerson = <O extends object, K extends keyof O>(
o: O,
k: K
): O[K] => {
return o[k];
};
どうでしょうか。
ちんぷんかんぷんではないでしょうか?
記法を言語化してみたらこんな感じ
でも大丈夫です。ちゃんと日本語で言語化してみせます。
- 関数の第一引数oはオブジェクトで型制限されたジェネリクスのO型をとる。
- 関数の第二引数kは引数oに含まれるオブジェクト内のキーで型制限されたジェネリクスのK型をとる。
- 関数の戻り値o[k]は、オブジェクトoのプロパティの中からキーがkに該当するものを探し、その値を返します。
・・・どうでしょうか。
もう少し掘り下げてみますね。
上記記法には何が使われているか
ここでは以下のようなものが使われています。
見ればわかるものから見てもわからんものまであります。
- ジェネリクス
- extends(型引数の制約)
- keyof
- Lookup Types
一つずつ解説します
ジェネリクス
まずはジェネリクスです。
上記例の<O extends object, K extends keyof O>
に該当する部分です。
ジェネリクスを簡単に説明すると、「型を変数みたいに扱おう」ということです。
引数にナンバー型や文字列型など何がくるかわからない場合に、弾力的に扱おうというものです。
extends(型引数の制約)
上記ジェネリクスによって型定義はされましたが、引数にどんな型がきてもオッケー☆としてしまうと実質的に型の意味を果たさなくなります。
そこでextendsの出番です。
これは、ジェネリクスで代わる代わるやってくる様々な型の引数に制限を加えるものです。
上の例だと、extends object
とextends keyof O
の部分です。
一つ目はオブジェクト型で制限し、二つ目は第一引数のオブジェクトに含まれるキー型で制限しています。
keyof
上の説明で出てきたkeyofについて説明します。
これについては順を追って説明します。
まず以下のようにオブジェクト型の変数を用意します。
const top = {
name: "Toproad",
age:20,
gender: "male",
};
keyofの前に、オブジェクト内のプロパティに型をつける必要がありますが、それがtypeofです。
type topTypeof = typeof top;
これで、type topTypeof = { name: string; age: number; gender: string; }
と型が付きます。(そもそも型推論されているので不要と言えば不要ですが・・・)
次に、このtypeof型を使ってkeyofを作成します。
type topKeyof = keyof topTypeof;
これにより、type topKeyof = "name" | "age" | "gender"
とキーがユニオン型となっている型が出来上がりました。
なお、以下のように一つにまとめることもできます。
// 上2行と同じ意味
type topTypeofKeyof =keyof typeof top;
Lookup Types
最後にLookup Typesです。
エクセルでもvlookupでおなじみの探しだすといったものです。
上の例だとreturn文の後ろのo[k]の部分で、keyofのキー型をキーに該当するプロパティを探し出し、
その値を取得するものになります。
記法を使った具体例
これまでの内容を踏まえた具体的な記載方法は以下のとおりです。
// オブジェクトの中から特定のキーに該当する値を返す関数式
const valueOfPerson = <O extends object, K extends keyof O>(
o: O,
k: K
): O[K] => {
return o[k];
};
const top = {
name: "Toproad",
age:20,
gender: "male",
};
// 以下は関数の呼び出しの具体例
// 例1:Toproadを出力します
console.log(valueOfPerson(top, "name"));
// 例2:20を出力します
console.log(valueOfPerson(top, "age"));
//例3:エラー⇒型 '"hogehoge"' の引数を型 '"name" | "age" | "gender"' のパラメーターに割り当てることはできません。
console.log(valueOfPerson(top, "hogehoge"));
【解説】
- 関数式valueOfPersonは、オブジェクトの中から特定のキーに該当する値を返す関数式です。
- valueOfPersonに対し、オブジェクトtopを使ってプロパティ値の取得を試みます。
- valueOfPersonの第一引数はobjectで型の制約をされているが、例1~3のいずれもオブジェクト型のtopが渡されており、制約と合致するので問題ない。
- valueOfPersonの第二引数はオブジェクト型のtopに含まれるキーのユニオン型"name" | "age" | "gender"で制約されており、例1・2は制約に含まれるnameとageなので問題ないが、例3のhogehogeは制約に含まれていないのでエラーを返される。
- 上記でエラーとなった例3を除いた例1・2だけがそれぞれ値を返す。(undefinedとなる)
【補足】
valueOfPersonを呼び出す際、valueOfPersonの後ろに<,>をつけていませんが、暗黙的に型が決められています。
まとめ
自分の説明で伝わったかわかりませんが、今回の記法の紹介で伝えたかったのは「複合的な要素を含んだコードを分解しながら理解することがスキルアップの近道」ということです。
もちろん負荷は大きいですが、こうしたコードを読み解く練習をすることがより実践で書く力をつけるためには大切だと思います。
参考にさせていただいたサイト
大変勉強になりました。ありがとうございます。
【TypeScript】Genericsの基礎
【TS】keyofを使ってオブジェクトと適切なキーを取る関数作成