🦔

TypeScriptの型パズルで遊ぶときのTips

7 min read

基本の操作

タプルから値をを取り出す。

type First<T extends any[]> = T extends [infer R, ... infer Rest] ? R : []


type __First_Ex1 = First<[1,3]>  // 1
type __First_Ex2 = First<[]>  // never
type Last<T extends any[]> = T extends [... infer _, infer Last] ? Last : []

type __Last_Ex1 = Last<[1,2,3]> // 3
type __Last_Ex2 = Last<[1]> // 1
type __Last_Ex3 = Last<[]> // never

文字列版

type First<T extends string> = T extends `${infer First}${infer Last}` ? First : never 


type __First_Ex1 = First<"abc">  // a
type __First_Ex2 = First<"d">  // d
type __First_Ex3 = First<"">  // never

再帰を使う

タプルの長さは[1,2,3]["length"]として出せるが、
今回は再帰によって導出してみる。

type LengthOfTuple<
  T extends any[] = [],
  Counter extends unknown[] = []
> =  T extends [infer _, ... infer Rest] ? LengthOfTuple<Rest, [...Counter, unknown]>  : Counter["length"]



type __LengthOfTuple_Ex1 = LengthOfTuple<[1,2,3]> // 3

空になるまで繰り返したり、型変数をつかって値を引き回したり、結果を保存できるのが分かる。

可読性をあげるために

型変数を使って正しい名前をつける

可読性のために型変数名はちゃんとつけた方が良い。anyである必要性がない場合は具体的な型を指定すると良い。

type NumberToTuple<T extends number, U extends any[] = []> R["length"] extends N ? R : NumberToTuple<N, [0, ...U]>

type Count = "count"
type NumberToTuple<T extends number, Counter extends Count[] = []> R["length"] extends N ? R : NumberToTuple<N, [Count, ...U]>

And (&&)

conditional type でandを使う場合はタプルを使って判定するといい。

// これは冗長
type BothString<A extends any, B extends any> = A extends string ? B extends  string ? true : false : false

type False = BothString<"",0>
type True = BothString<"foo", "bar">
type BothString<A extends any, B extends any> = [A, B] extends [string, string]? true : false

type False = BothString<"",0>
type True = BothString<"foo", "bar">

Or (||)

同様にorの判定も

type BothString<A extends any, B extends any> = [A, B] extends [string, any] | [any, string] ? true : false
// こっちの方が好き。
type BothString_<A extends any, B extends any> = [A, B] extends {0: string} | {1: string } ? true : false

type False = BothString<0,0>
type True_ = BothString<0,"">
type True1 = BothString<"",0>
type True2 = BothString<"foo", "bar">

配列が空になるまで繰り返す。

配列が空であるは以下のように判定できる。

type IsEmpty1<T extends any[]> = T["length"] extends 0 ? true : false

type IsEmpty2<T extends any[]> = T extends [] ? true : false

type IsEmpty3<T extends any[]> = T extends [infer First, ...infer Rest] ? false : true

型のキャスト

以下のようなAdd型がある。

type NumberToTuple<N extends number, R extends any[] = []> = R["length"] extends N ? R : NumberToTuple<N, [unknown, ...R]>
type Add<A extends number, B extends number> = [...NumberToTuple<A>, ...NumberToTuple<B>]["length"]
// type Five = Add<2, 3> // 5

これをnumber型を型変数に取るNumberToString型で使いたい。

しかしAddの結果をそのまま使おうとすると、Add<A, B> 型はnumberとして推論されないため、エラーになる。

type NumberToString<T extends number> = `${T}`

type NG<A extends number, B extends number> = NumberToString<Add<A, B>> // Type 'Add<A, B>' does not satisfy the constraint 'number'.

回避策として素直にT extends numberで推論するやり方がある。

type OK_1<A extends number, B extends number> =  Add<A, B> extends number ? NumberToString<Add<A, B>> : never

// 型変数を使った方がスッキリ書ける
type OK_2<A extends number, B extends number, Added = Add<A, B>> = Added extends number ? NumberToString<Added> : never

これはExtractを使うともっと簡潔に書ける。


type OK_3<A extends number, B extends number> = NumberToString<Extract<Add<A, B>, never>>
type _OK_3_Limit = OK_3<43,43>

最初からExtractする方針にすると使う側がシンプルになって良い。

type NumberToTuple<N extends number, R extends any[] = []> = R["length"] extends N ? R : NumberToTuple<N, [unknown, ...R]>
type Add<A extends number, B extends number> = Extract<[...NumberToTuple<A>, ...NumberToTuple<B>]["length"], number>
// type Five = Add<2, 3> // 5


type NumberToString<T extends number> =`${T}`

type OK<A extends number, B extends number> =  ShouldNumber<Add<A, B>>


// type _OK__limit = OK_2<43,43> // Type instantiation is excessively deep and possibly infinite.(2589)

ただ可読性が良くなる一方でスタックの上限があがってしまう。

``

再帰型の限界を超える。

再帰を使って型定義をすると、限界にぶち当たる。

例えばAdd型の場合は43+43の86が最大になる。

type NumberToTuple<N extends number, R extends any[] = []> = R["length"] extends N ? R : NumberToTuple<N, [unknown, ...R]>
type Add<A extends number, B extends number> = [...NumberToTuple<A>, ...NumberToTuple<B>]["length"]


type __Add_Limit = Add<43,43>  // 86が限界。

ただ、87以上の和の計算ができないかというとそうでもない。
横に増やすことが可能だ。

type Add_Three<A extends number, B extends number, C extends number> = [...NumberToTuple<A>, ...NumberToTuple<B>, ...NumberToTuple<C>]["length"]

type __Add_Limit = Add<43,43, 1>  // 各変数には43までしか入れられないが、横に増やすことはできる。

なので、うまい具合に分割する再帰と組み合わせることでもう少し先に進める。

例としてNumberToTupleの限界を超えてみる。

といっても今の実装でも限界は超えてる。

第2引数のResultに値を指定することで46以上を出すことができた。

type NumberToTuple<N extends number, R extends any[] = []> = R["length"] extends N ? R : NumberToTuple<N, [unknown, ...R]>

// type _NG = NumberToTuple<46> // Type instantiation is excessively deep and possibly infinite.(2589)

// 46を作れた。
type _Over46 = NumberToTuple<46, NumberToTuple<1>>["length"]

なので、これを再帰的につかう型を用意すれば良い。

そのためには上限が来る前にNumberToTupleを返すようにしてあげないとならない。

なのでこう改良してあげる。

type NumberToTuple_With_Limit<
  N extends number,  
  Limit extends number, 
  PreviousResult extends unknown[] = [],
  Counter extends unknown[] = []
> = PreviousResult["length"] extends N ? PreviousResult : 
    Counter["length"] extends Limit ? PreviousResult : // 最大試行回数 
    NumberToTuple_With_Limit<N, Limit, [...PreviousResult, unknown], [unknown, ...Counter]>


type __Ex1 = NumberToTuple_With_Limit<5, 10>["length"] // 5
type __Ex2 = NumberToTuple_With_Limit<100, 40>["length"] // 40

これを使うとLimitを超える場合は一旦再帰を抜ける用になる。

これを使うことで

type NumberToTuple_With_Limit<
  N extends number,  
  Limit extends number, 
  PreviousResult extends unknown[] = [],
  Counter extends unknown[] = []
> = PreviousResult["length"] extends N ? PreviousResult : 
    Counter["length"] extends Limit ? PreviousResult : // 最大試行回数 
    NumberToTuple_With_Limit<N, Limit, [...PreviousResult, unknown], [unknown, ...Counter]>


type Limit = 10

type NumberToTuple<
  N extends number, 
  PreviousResult extends unknown[] = [],
  NextResult extends unknown[] = NumberToTuple_With_Limit<N,Limit, PreviousResult>
> = PreviousResult["length"] extends N ? PreviousResult : 
    NumberToTuple<N, NextResult>



type __100 = NumberToTuple<100>["length"] / 100

長さ100とかのタプルを生成できるようになった。

今回はLimit=10にしたが、多すぎると横の再帰の数が減るし、少なすぎても縦の再帰量がへる。
26~29後半くらいが最適な値だった気がする。