9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScriptの再帰型定義で遊んでみた

Last updated at Posted at 2019-07-05

はじめに

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, []>>

NumberInRangeRec0...N-1 の Union を作ります。
カウント型変数を再帰毎に Union して1つずつ範囲を広げていきます。

NumberInRange の定義側では簡略式で書くと以下のように型を展開しています

  1. (0...m-1 | m) - (never...n-1)
  2. (0...m) - (never...n-1)
  3. 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がパワーアップしてこういう型が実際使えたら楽しいですよね、ならないかなぁ

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?