TS 4.1 の Template Literal Types を使うと、文字列を解釈して型定義ができる。文字列は変数化でき、テンプレートリテラルで埋め込める。
type Hello = 'Hello'
type World = 'World'
// Template Literal を埋め込める
type HelloWorld = `${Hello}, ${World}`
// => type 'Hello, World'
// 引数に取ることもできる
type Concat<T extends string, S extends string> = `${T}, ${S}`
type HelloWorld = Concat<'Hello', 'World'>
// => type 'Hello, World'
やばい。革命的だ。
更にやばいのが、 infer
を使って任意の文字列にマッチ、みたいなことができる!
type GetWorld<T extends string> = T extends `Hello, ${infer World}` ? World : never
type World = GetWorld<`Hello, World`>
// => type 'World'
infer
は一つのリテラルで複数回使えるので、簡単な SQL くらいならすぐパースできる。以下は型レベルでSQLがパースできる例。
infer
と再帰的な型定義を使えば、ツリー構造を型レベルでパースしたり、文字列を分割することも可能。(後で例を書きます)
既に SQL から型定義を作成したり、 GraphQL の Resolver の型定義をするライブラリが作成されつつある。
2年前、 typed-graphqlify という OSS を作ったが、Template Literal Types を使えば、もっとすごいものが作れる。
試しに GraphQL から型定義を自動作成するものを、ちょっと書いてみた。
/*
* TypeScript 4.1
*/
type GetSchema<Schema> =
Schema extends `type Query { ${infer RootQueryName}: String! }`
? { [k in RootQueryName]: string }
: never
type QueryBuilder<Schema, Query> = Query extends `{ ${infer RootQueryName} }`
? RootQueryName extends keyof Schema ? { [k in RootQueryName]: Schema[RootQueryName] } : never
: never
type Schema = GetSchema<`type Query { hello: String! }`>
type ExpectQueryType = QueryBuilder<Schema, '{ hello }'> // => { hello: string }
type ExpectNever = QueryBuilder<Schema, '{ foo }'> // => never
見た目はえげつないけど、ちゃんと動いている。がんばればライブラリ化できるかもしれない。 typed-graphqlify の次期バージョンとして作ろう。
ただ、 Fragment とか面倒そうw
頑張る。
[追記]
実装のヒントを貰うために、 graphql-typed-ast に Issue を立てた。良い人だ。
https://github.com/dotansimha/graphql-typed-ast/issues/2
[/追記]