はじめに
TypeScript の型定義の自由さはハマる面白さがありますよね。
TypeScript のリテラル型と Union Types で 1 | 2 | 3...
と書けるのを初めて見たとき
型バリデーションができる!?
とワクワクしたのですが、0~100 の範囲ともなるとさすがに書いてられません。
NumberInRange<0, 100>
なんて書けたらな~、と思っていました。
ただ TypeScript では再帰型定義が難しく作るのは諦めかけていたのですが
こちらの記事で再帰型定義ができることを知り、なんとか作れました!圧倒的感謝。
TypeScript で再帰的な型を定義する
TypeScriptで最低n個の要素を持った配列の型を宣言する方法
その途中で遊んでて出来た型と一緒に紹介しておきます。
n個の要素を持つ配列の型
紹介した記事で最低n個以上があったので、n個ピッタリも欲しいということで作ってみました。
再帰中では作るタプル自体をカウントにできるので、n個以上よりも簡単になりますね。
type Append<Elm, T extends unknown[]> = ((
arg: Elm,
...rest: T
) => void) extends ((...args: infer R) => void)
? R
: never
type TupleNRec<Num, Elm, T extends unknown[]> = {
0: T
1: TupleNRec<Num, Elm, Append<Elm, T>>
}[T['length'] extends Num ? 0 : 1]
// n個の要素を持つタプル型
type TupleN<N extends number, T> = TupleNRec<N, T, []>
使用例
// TupleN<3, number> -> [number, number, number]
const t1: TupleN<3, number> = [0, 1] // Error!
const t2: TupleN<3, number> = [0, 1, 2] // OK
const t3: TupleN<3, number> = [0, 1, 3, 4] // Error!
数値リテラル型のインクリメント
リテラル型の演算で const num: 1 + 1
なんてことはできないので、代わりにインクリメント型を作りました。
先ほどと同じようにの n個のタプルを作って1つ要素を追加して length
を取得します。
以降 Append
は定義してあるものとして省略します。
type IncRec<Num, T extends unknown[]> = {
0: Append<unknown, T>['length']
1: IncRec<Num, Append<unknown, T>>
}[T['length'] extends Num ? 0 : 1]
// 数値リテラル型に +1 する
type Inc<N extends number> = IncRec<N, []>
使用例
// Inc<10> -> 11
const n1: Inc<10> = 11 // OK
const n2: Inc<10> = 10 // Error!
本当は TupleN
を使いまわせたら良いのですが、直接型引数を再帰型へ渡すと無限再帰になってエラーになります。
またオブジェクトで包む必要があるので面倒で止めました。
type Inc<N extends number> = Append<unknown, TupleN<N, unknown>>['length'] // Error!
数値リテラル型のデクリメント
反対のデクリメントも作りました。
最後に Append
と逆に Tail
で要素を1つ取り除いてあげます。
// Tuple の先頭以外を取り出す
type Tail<T extends unknown[]> = ((
...args: T
) => void) extends ((head: unknown, ...args: infer R) => void)
? R
: never
type DecRec<Num, T extends unknown[]> = {
0: Tail<T>['length']
1: DecRec<Num, Append<unknown, T>>
}[T['length'] extends Num ? 0 : 1]
// 数値リテラル型に -1 する
type Dec<N extends number> = DecRec<N, []>
使用例
// Dec<10> -> 9
const n1: Dec<10> = 9 // OK
const n2: Dec<10> = 10 // Error!
n...mの範囲の内1つの数値を表す型
念願の型範囲バリデーションです。
// 0...N-1 の Union を作る
type NumberInRangeRec<Num, T extends number, Count extends unknown[]> = {
0: T
1: NumberInRangeRec<Num, T | Count['length'], Append<unknown, Count>>
}[Count['length'] extends Num ? 0 : 1]
// n...mの範囲の内1つの数値を表す型
type NumberInRange<N extends number, M extends number> =
Exclude<NumberInRangeRec<M, 0, []> | M, NumberInRangeRec<N, never, []>>
NumberInRangeRec
で 0...N-1
の Union を作ります。
カウント型変数を再帰毎に Union して1つずつ範囲を広げていきます。
NumberInRange
の定義側では簡略式で書くと以下のように型を展開しています
(0...m-1 | m) - (never...n-1)
(0...m) - (never...n-1)
n..m
0...m-1 | m
の部分は 0...N-1
に合わせつつ未満でなく以下にするためですが、
0...N
を返す NumberInRangeRec
を作っても良いかもしれません。
0...n-1
でなく never...n-1
を使っているのは、 NumberInRange<0, M>
の指定の際に 0...n = 0...0
では 0
自体が Union の対象から外れてしまうので、その回避のために使っています。
never
を渡せば再帰0回ならそのまま never
が返ってきて Union しても影響がないということですね。
使用例
// NumberInRange<10, 30> -> 10 | 11 | 12 | ... 30
const n1: NumberInRange<10, 30> = 9 // Error!
const n2: NumberInRange<10, 30> = 20 // OK
const n3: NumberInRange<10, 30> = 31 // Error!
型再帰型も作りたかった
ついでに、これだけ再帰を書いてると一般化したいんですけど、こんな感じの型アロー関数 type<T> => Type<T>
的な何かが無いと無理な気がして諦めました。
type Rec<Num extends number, Elm, Target, Transform, Count extends unknown[]> = {
0: Target
1: Rec<Num, Elm, Target, Transform<Elm, Target, Count>, Count>
}[Count['length'] extends Num ? 0 : 1]
type TupleN<N extends number, T> = Rec<N, T, [], type<Elm, Target> => Append<Elm, Target>, []>
残念なお知らせ
ここまで書きましたが、これら全部実用では使えません。
再帰回数の限度があり、自分の場合42回までしか再帰してくれませんでした。
つまり NumberInRange<0, 100>
は無理でした、NumberInRange<0, 41>
で限界です。
はい、自己満足です。でも型バリデーションの夢を見たかったのよ。。。
残念😭
でもTypeScriptがパワーアップしてこういう型が実際使えたら楽しいですよね、ならないかなぁ