そもそも型パズルとは?
<「型」を受け取り「型」を出力する > 関数の型バージョンのようなものを作る問題。
今回それをやってみたのですが、TypeScriptに関する基礎的な知識があっても
最初独特の慣れが必要だなと感じたので書かせていただきます。
何が最初難しいのか
基本的に type YOUR_ANSWER = nantara
としてYOUR_ANSWERという型を用意することになります。
すると...のところでは例えば以下のことができません。
- forループを回す
- オブジェクトなどのインスタンスを直接参照する
型関数を作るときの入力は、所詮「型」でしかありません。
type YOUR_ANSWER<T> = nantara
として右辺でジェネリクスに入れたTをこねくり回すわけなのですが、
例えばTがオブジェクトの時、型情報に含まれる「オブジェクトのキー」を参照したいときはどうすればいいのでしょうか。
オブジェクトの型情報からキーを取り出すには
結論から書くと"in"と"keyof"を使います。
type Dog = {
name: string;
run: () => void;
};
type PartialDog = {
[P in keyof Dog]?: Dog[P];
}
例えばこういったコードがあったとしたらP in keyof Dog
とすることで
Dogのキーが順々に取り出されます。
PartialDog は { name?: string, run?: () => void }
というオブジェクトの型になります。
ではtupleのインデックスを取り出すには?
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple>
このようにしたときにresultの持つ型情報が、
オブジェクト
{ tesla: 'tesla', 'model 3': 'model 3' //略 }
のようになるには、どうしたらいいのでしょうか。
いきなりだと難しいのでいくつかに分解します。
やることは、
- tupleの各要素にアクセスする
- それを順番にオブジェクトのキーにする
- プロパティも同じ
ちなみにさっきの手は使えません。
type TupleToObject<T extends string[]> = {
[P in keyof T] : T[P]
}
Tがさっきと違いtupleなので Pは順々にtupleのインデックスになります。
ちなみに自分はここで問題に触れました(製作者様とは何の関係もありません)
tupleが曖昧な人向け
概要
tuple型は[”型1”, “型2”, “型3”…]のように書きます。
特徴としては、一度定義されると
- 要素の数が変わらない
- 各要素の型が変わらない
ことです。
問題に戻る
tupleの型情報を順に取得するには?
流れとしてはこのようになります。
- tupleの全ての要素をユニオンさせる
- それを"in"で順に取り出す
コードとしては以下になります。
type TupleToObject<T extends string[]> = {
[P in T[number]] : P
}
T[number]は 'tesla' | 'model 3' | 'model X' | 'model Y'
という型になります。「リテラル型」の「ユニオン型」です。
{
[P in T[number]] : P
}
とすることで { tesla: 'tesla', 'model 3': 'model 3' // 略}
というオブジェクトが作られます。
(teslaだけクォートで囲まれない。model 3は間にスペースがあるので囲まれているだけ)
また、 P in T[number]
が[]
で囲まれているのは、この記法の約束だと思ってください。
またこちらの記事も参考になります。
答え
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type TupleToObject<T extends readonly string[]> = {
[K in T[number]] : K
}
type result = TupleToObject<typeof tuple> // { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
tupleはreadonlyの配列か?
例えばさきほど、
type TupleToObject<T extends string[]> =
のように書きましたが
type TupleToObject<T extends readonly[]> =
もしtupleがreadonlyの配列ならこう書いてもいいのではないでしょうか。
ですがtupleは、
要素の合計数と 各「インデックス」に与えられた型
が変わらないだけです。インデックス1が、stringと指定されていたら、
それを新しい要素(同じく型はstring)に変更する事ができます。
ですが
type TupleToObject<T extends readonly string[]> =
としたらどうでしょうか。これは "変更不可で全ての要素がstringの配列" を受け取ります。
(結局配列だが、例のタプルに最も近い定義だと考えられる)
実際先ほど貼りましたmosyaの回答では簡便のためかany []
で制約されているのですが、
さらにその元ネタとなっているgithubの回答で最も評価を得ているのはextends readonly string[]
でした。
補足 型定義にforループはない について
確かにforループは使えないのですが、Recursive Typesという概念があり、
型定義の中で「自分」を参照することで、リスト・ツリー・グラフなどのデータを表現できます。
ツリー構造のコードを紹介
type Tree<T> = {
value: T;
children: Tree<T>[];
};
const aTree: Tree<number> = {
value: 1,
children: [
{
value: 2,
children: []
},
{
value: 3,
children: [
{
value: 4,
children: []
}
]
}
]
};
具体的な例だとJSONに対しての定義に使われるみたいです。