🍊

MappedTypeを使って出来ること

に公開

経緯

MappedTypeってなんぞや?ってところから始まったのだけど

type MappedType<T> = {
    [K in keyof T]: T[k]
}

みたいなやつで、PromsieAllを作ってるときに色々とお世話になったので、少し掘ってみた。

中身の話

サンプルコードになるので、詳しくは見てもらったり、VSCode等で型の確認をしてもらうと、何が起こってるのかわかりやすと思います。

コード

https://github.com/risk/ts-playground/blob/main/src/mappedType/mappedType.ts

列挙定義

このあと色々使うので、まずは列挙を定義しておきます
https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L7-L13
今回は、楽しく学ぶためにフルーツを題材に。

Record型の話

MappedTypeを扱うのに切っても切り離せないのがRecordで、まずはRecordを定義するところからはじめています。
https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L15-L28
フルーツごとにパラメータを入れてみたり、先に空っぽの構造つくってから、後入れしてみたり。
Record型は、{}での初期化ができない(値が入っていることを保証するので、空っぽの定義が有りえないと判断されちゃう)ので、asで型の情報あててやることで作るところとかは、たまに使うケースがあるかもしれない。

ここからMappedTypeの話

https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L31-L36
まずは、Recordの分解から。
そもそも「RecordTypeってなんやねん?」って部分で、元の定義を引っ張ってきた

type Record<K extends keyof any, T> = { [P in K]: T; }

こんな感じなのね。読み解くと
Kは、keyofの結果に制約されていて、そのキーに対して、T型の値を設定出来る型。
要するに、KVS(Key-Value Store)ってことになりますね

この型定義はこのあといっぱいでてくるのでお楽しみに。

as const の話

よくしらんけどつけないケースがある!でよく知られている as const ですが
コイツも多少理解が必要なので、サンプルを書いています。
https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L38-L58
実際に型を見てみると色々わかるので、眺めてみると面白いですね。

constをつけるケースとつけないケースで見比べてみると、

  • constをつけない場合は、その値を入れられる型に広げる動作
  • constをつけた場合は、その値しか入らない型に留める動作

となります。

つまり、型を固定する(広げない)を指示するってイメージなんですね。
このあと、型を広げられると困るケースが結構あるので、そのたびにでてきます。

MappedType 型変更

ここでは、元の型を参考にして、型を配列にしています。
https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L61-L73
一体どういうことなんだと、最初まじで迷いましたが、読み解いていきます。

type ArrayMultiType<T> = {
  [K in keyof T]: T[K][]
}

たとえばコレ。Jsonは、a.apple みたいに . で利用する他に、a['apple']でも使えます。
コレと同じ動作が型の世界でも起こっていると考えるとわかりやすいです。

つまり、Tという型があるときに、T の Keyの一覧 を K とし、それをKeyの定義とする
このときのKをKeyの集合として扱えるので、T[K]で それぞれのKeyに対応した型 に対して
[](配列)である定義に変えてあげる。
(これ『lookup type』と呼ばれるものだそうです、博学だなChatGPT)

これで、いままで単独の要素だったものが、配列の型に書き換わるわけです。

実際のサンプルの方でも、見ていただくと、それぞれの型ごとに配列に変わっているところ見られます。

MappedType もうすこし大きく変えてみる

https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L75-L105
さっきのを参考に、今度は型をもっとガッツリ変えて、関数とその引数を格納できる方に書き換えてみました。
配列に変えた型をそのまま引数に差し込み、戻り値にも差し込み、引数自体にも差し込み。
これであれば、関数側との引数とargsで定義した引数が一致していると型レベルで整合性がとれたりします。

関数に変えたやつを呼び出してみる

https://github.com/risk/ts-playground/blob/86df52d5335d32e7297adcdfce6d10c15c9ccbf9/src/mappedType/mappedType.ts#L107-L131
最後に、実際に呼び出して動作確認できるようにしておきました。
型の制約でテーブルの型を縛っておき、キーの一覧取得と関数呼出しを行っています。
一見危険そうに見える(関数定義が「(...args: any[]) => any」ですしね)んですが、型の時点で関数と引数の整合性はとっているので、合わせて使う分には問題は起こらないといえます。
ただ、型安全かというと、そういうわけじゃないので、本当はもっと工夫したほうが良いのかも?
(keyedPromiseAllv2では、そのあたりを守るために、クラス化してたりします)

で、実際に呼び出しますと、関数がちゃんと呼べますよ というサンプルでした。

最後に

型の仕組みとして、MappedTypeは面白いんですが、複雑なものでもあると思います。
使い方次第で、普通読めないだろって感じのコードになることも多いのかなと思いますので、使い所を見極めてつかっていくと、非常に強力なのかなと思いました。
少なくとも広い範囲に見えるように使うのは違うだろうなとは思っていますw

Discussion