※この記事に含まれる内容は TypeScript 4.1 のプレビュー版のものです. 今後仕様が変わり動かなくなる可能性もありますのでご注意ください.
話題の template string types で早速遊んでみます.
ゴール
.foo[1].bar
といった形のパス文字列を型レベルで解析してクエリしちゃいます. こういう感じ:
type R1 = Query<{ foo: number }, "">; // R1 = { foo: number } type R2 = Query<{ foo: number }, ".foo">; // R2 = number type R3 = Query<[number, string, boolean], "[1]">; // R3 = string type R4 = Query<{ foo: { bar: string } }, ".foo.bar">; // R4 = string type R5 = Query<[[number, string], [boolean, undefined]], "[1][0]">; // R5 = boolean type R6 = Query<[{ foo: [[{ bar: string }]] }], "[0].foo[0][0].bar">; // R6 = string type RE = Query<{ foo: number }, "[">; // RE = never type RU = Query<{ foo: number }, ".bar">; // RU = unknown
.foo.bar
のようなドットつなぎのみのパスについては PR でも挙げられていますが, [1]
のようなパスにも同時に対応するにはどうしたらよいか, という話.
やっていき
まずはキーによる参照を定義しておきます. わざわざこれを定義するのはキーが存在しなかった場合の扱いを一括で決めておくため. Subtyping のことを考えると undefined
ではなく unknown
が妥当でしょう.
type Access<T, Key extends string> = Key extends keyof T ? T[Key] : unknown;
続いてクエリ実行部分本体です. ウワー.
type Sep = "." | "[" | "]"; type Query<T, Path extends string> = Path extends "" ? T : Path extends `.${infer Key}${Sep}${infer Rest}` ? QueryDot<T, Path, Key> : Path extends `[${infer Key}]${Sep}${infer Rest}` ? QueryBracket<T, Path, Key> : Path extends `.${infer Key}` ? Access<T, Key> : Path extends `[${infer Key}]` ? Access<T, Key> : never; type QueryDot<T, Path extends string, Key extends string> = Key extends `${infer A}${Sep}${infer B}` ? never : Query<Access<T, Key>, Path extends `.${Key}${infer Rest}` ? Rest : never>; type QueryBracket<T, Path extends string, Key extends string> = Key extends `${infer A}${Sep}${infer B}` ? never : Query<Access<T, Key>, Path extends `[${Key}]${infer Rest}` ? Rest : never>;
順番に見ていきましょう. まずは Sep
はパス中の区切り文字を定義しています.
type Sep = "." | "[" | "]";
続いて Query
. 一行目は空文字列ならそのまま T
を返すだけ.
type Query<T, Path extends string> = Path extends "" ? T : ...;
後ろの三行もまあ簡単で, .foo
や [1]
のようなパスを見てアクセスするだけです.
type Query<T, Path extends string> = ... : Path extends `.${infer Key}` ? Access<T, Key> : Path extends `[${infer Key}]` ? Access<T, Key> : never;
問題は真ん中の二つ. まずは .foo
のあとにさらにパスが続く場合の処理を見てみましょう.
type Query<T, Path extends string> = ... : Path extends `.${infer Key}${Sep}${infer Rest}` ? QueryDot<T, Path, Key> : ...;
もしここに Path
として .foo[1].bar
が入ってきた場合, Key
は "foo" | "foo[1" | "foo[1]"
となります. Sep
が三種類あるからですね. 一方でこれらの Key
のうち正しいもの (最短でマッチしているもの) は "foo"
です. ではこれをどう取り出しているかというのが QueryDot
の部分.
type QueryDot<T, Path extends string, Key extends string> = Key extends `${infer A}${Sep}${infer B}` ? never : Query<Access<T, Key>, Path extends `.${Key}${infer Rest}` ? Rest : never>;
まず conditional type で union type になっている Key
を分解しつつ, Key
の中に Sep
が含まれるような場合は never
を返すことで除いています. そして Sep
が含まれない場合は Key
を使ってアクセスしつつ, 残りを Path
から抜き出して再度 Query
に渡しています. 簡単ですね.
[1]
のあとにさらにパスが続く場合もほとんど同じ. (追記: これ ]
で区切られているのでわざわざこんなことしなくてもよいのでは?)
type Query<T, Path extends string> = ... : Path extends `[${infer Key}]${Sep}${infer Rest}` ? QueryBracket<T, Path, Key> : ...; type QueryBracket<T, Path extends string, Key extends string> = Key extends `${infer A}${Sep}${infer B}` ? never : Query<Access<T, Key>, Path extends `[${Key}]${infer Rest}` ? Rest : never>;
これで完成.
type R1 = Query<{ foo: number }, "">; // R1 = { foo: number } type R2 = Query<{ foo: number }, ".foo">; // R2 = number type R3 = Query<[number, string, boolean], "[1]">; // R3 = string type R4 = Query<{ foo: { bar: string } }, ".foo.bar">; // R4 = string type R5 = Query<[[number, string], [boolean, undefined]], "[1][0]">; // R5 = boolean type R6 = Query<[{ foo: [[{ bar: string }]] }], "[0].foo[0][0].bar">; // R6 = string type RE = Query<{ foo: number }, "[">; // RE = never type RU = Query<{ foo: number }, ".bar">; // RU = unknown
はい.