完成したもの
// このようなデータがあるとき、
const ProgrammingLanguages = {
FRONTEND: 'TypeScript',
BACKEND: {
SERVER_SIDE: 'Rust',
DATABASE: {
SQL: 'PostgreSQL',
NoSQL: 'DynamoDB'
}
},
MOBILE: {
ANDROID: 'Kotlin',
IOS: 'Swift'
}
} as const;
// 型パラメータにパスを渡すことで、それ以下の値だけを受け付ける文字列リテラル型を作成できる。
type LangFrontend = Language<'FRONTEND'>; // 'TypeScript'
type LangMobile = Language<'MOBILE'>; // 'Kotlin' | 'Swift'
type LangDatabase = Language<'BACKEND.DATABASE'>; // 'PostgreSQL' | 'DynamoDB'
type LangSql = Language<'BACKEND.DATABASE.SQL'>; // 'PostgreSQL'
コード
type Recursive<T, K> = K extends `${infer A}.${infer B}`
? A extends keyof T
? Recursive<T[A], B>
: never
: K extends keyof T
? T[K] extends string
? T[K]
: Recursive<T[K], keyof T[K]>
: never
;
type Language<K> = Recursive<typeof ProgrammingLanguages, K>;
解説
K extends `${infer A}.${infer B}`
渡されたキーが .
を含む場合、 Recursive<T[A], B>
で一つ深いネストに対し再帰的に同じ処理を行う。
'BACKEND.DATABASE.SQL'
のように複数含む場合は最初の .
の前後で区切られる('BACKEND.DATABASE.SQL'
→ 'BACKEND'
& 'DATABASE.SQL'
)。
: K extends keyof T
? T[K] extends string
? T[K]
: Recursive<T[K], keyof T[K]>
: never
渡されたキーが .
を含まない場合、値が string
型になるまで再帰して探索し、それらのユニオン型を作成する。
おまけ
型パラメータに渡すパスの候補を出してくれるようにする。
以下のコードは、キーを再帰的に探索し .
で繋げたすべてのパターンのユニオン型を作成している。
type Join<A extends string, B extends string> = A extends '' ? B : `${A}.${B}`;
type Path<T, P extends string = ''> = keyof T extends infer K extends keyof T
? K extends string
? Join<P, K> | (T[K] extends string ? never : Path<T[K], Join<P, K>>)
: never
: never;
type Language<K extends Path<typeof ProgrammingLanguages>> = Recursive<typeof ProgrammingLanguages, K>;
まとめ
TypeScript の型システムは慣れれば面白いものがたくさん作れそうです。
よき TypeScript ライフを!