はじめに
この記事はHRBrain Advent Calendar 2022カレンダー2の7日目の記事です。
株式会社HRBrainでは毎週TypeChanllengeを行っています。
この取り組みを紹介した記事があるので興味がある方は御覧ください。
その中で「Permutation」という特段難しい問題に詰まったので紹介します。
復習がてら記事にしました。
type-challengesとは
TypeScriptの型の問題集のようなリポジトリです。
Vue.js コアチームメンバーでVitest、Slidevの作成者である@antfu7さんという方が開発しています。
Permutation
Union 型を Union 型の値の順列を含む配列に変換する順列型を実装する問題です。
type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
参考にさせていただいた回答
こちらが最もコメントの多く、recommendのラベルが付けられている回答です。
本記事もこちらを参考にさせて頂いています。
丁寧に解説してくださっているので、興味がある方は下記のリンクを御覧ください。
type Permutation<T, K = T> = [T] extends [never]
? []
: K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never;
解説
まとめては理解しにくいと思うので分けて解説します。
[T] extends [never] ? [] : K
type Permutation<T, K=T> =
[T] extends [never] ? [] : K
もう既にわけがわかりませんね
これは一言でいうとTがneverか判定している処理です。
下記の式ででいけるのでは?と思う方がいるかも知れませんが、期待通りの動作はしません。
TSのPlaygroundを見ていただくと理解が早いと思います。
T extends never ? [] : K
型引数に渡されたneverは条件分岐の際、無視されるので機能しません。
そのため評価される前にタプル型にして、条件分岐できるようにしています。
かなりhack的な方法ですが、neverを判別するにはこれしか無いようです。
詳しくはこちらをご覧ください。
K extends K
type Permutation<T, K = T> = K extends K
? [K, ...Permutation<Exclude<T, K>>]
: never;
TypeScriptはunion型を型引数に渡すと下記のように分散する性質があります。
T extends U ? X : Y
T = 'A' | 'B'
(A extends U ? X : Y) | (B extends U ? X : Y)
それを利用してK extends K
にunion型を渡してそれぞれ条件分岐を行わせることができます。
このように渡されたunion型でループ処理のようなことをさせています。
? [K, ...Permutation>]: never
[K, ...Permutation<Exclude<T, K>>]
'A'
が型引数K
に渡されたとします。
再帰的にPermutationの引数としてT('A' | 'B' | 'C')
からK('A')
を除外した'B' | 'C'
を型引数として渡しています。
配列に K('A')
とPermutationの結果を展開しています。
この再帰的な流れを理解するのに下記の表がとてもわかりやすかったのでぜひご覧下さい。
最終的に全てのパターンが結合され順列を網羅した型になります。
['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']
さいごに
type-challengesをやっていて着実に型表現力がついてきていると日々実感しています。
TypeScriptに自身がない方、TS力を上げたい方は一度チャレンジしてみてはいかがでしょうか?