TypeScript型レベルプログラミングの細かいテクニック
TypeScriptで込み入った型レベルプログラミングをする上で、もしかしたら役に立つかもしれないニッチなテクニックを雑にメモする場所です。
実案件で役立つものというより、型レベルですごい魔法なようなことをするライブラリを作るときに使うようなものを中心に載せています。
長いタプル型も短いタプル型に代入できない
2項タプルは1項タプルに代入できても良さそうだが、できないようになっている。
const tuple2: [any, any] = [null, null];
const tuple1: [any] = tuple2;
代入不可の理由
-
length
が互換していないため
-
sort
,fill
,copyWithin
メソッドが互換していないため
ただし、readonly
なタプルではsort
などがないため、これらの互換性は関係なくなる。
type Tuple1 = readonly [any];
type Tuple2 = readonly [any, any];
型レベルのテストで型が一致するかテストする方法
次のような関数を定義して、比較する型を型引数に与える。一致を期待する場合は、関数の引数にtrue
を、不一致を期待する場合はfalse
を与える。
declare function assertSame<A, B>(
expect: [A] extends [B] ? ([B] extends [A] ? true : false) : false
): void;
使い方:
型レベルのJestっぽいもの
型レベルで型の代入可能性をテストするJestっぽいAPIのものを作ってみた。
declare function expect<A>(): {
toBe<B>(): IsSame<A, B> extends true
? OK
: "Expected that A and B should be assignable to each other" & {
A: A;
B: B;
};
toBeAssignableTo<B>(): IsAssignable<A, B> extends true
? OK
: "Expected that A should be assignable to B" & { A: A; B: B };
notToBeAssignableTo<B>(): IsAssignable<A, B> extends false
? OK
: "Expected that A should not be assignable to B" & { A: A; B: B };
};
type IsSame<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
type IsAssignable<A, B> = [A] extends [B] ? true : false;
type OK = { ok: "ok" };
使い方
expect<1>().toBeAssignableTo<number>().ok;
expect<number>().notToBeAssignableTo<1>().ok;
expect<1>().toBeAssignableTo<1>().ok;
expect<number>().toBeAssignableTo<1>().ok;
FAILのときはok
を覗くと、デバッグに使える情報が見れる
交差型のオブジェクトを合体させて、単一のオブジェクト型にする
{a: any} & {b: any}
を{a: any, b: any}
にするユーティリティ型です。
type Merge<T extends object> = { [K in keyof T]: T[K] };
{a: any} & {b: any}
も{a: any, b: any}
も意味的には同じですが、型レベルプログラミングをしていてエディタ上の可読性が微妙なときは、このMerge
が役立ちます。
↓交差型そのままの場合の見た目:
↓Merge
したあとは、ちょっと読みやすくなる
Object.assign
っぽいことを型レベルでやる
type ObjectAssign<Target extends object, Source extends object> = {
[Key in keyof Target | keyof Source]: Source extends { [_ in Key]: any }
? Source[Key]
: Target extends { [_ in Key]: any }
? Target[Key]
: never;
};
型レベルでローカル変数っぽいことをする
型変数にもローカル変数が欲しくなることがある。
ローカル変数の構文はないが、次のようなinfer
を使を用いたイディオムを書くと、ローカル変数っぽいものが作れる。
値 extends infer 変数名
? 変数名を参照する処理
: never
具体例
type UppercaseRepeat<T extends string, Repeat extends 1 | 2 | 3> =
Uppercase<T> extends infer Text // Textがローカル変数のように使える
? Repeat extends 1
? [Text]
: Repeat extends 2
? [Text, Text]
: Repeat extends 3
? [Text, Text, Text]
: never
: never; // 代入が失敗することはないが、分岐としては必要
type T1 = UppercaseRepeat<'hello', 3>;
//=> ["HELLO", "HELLO", "HELLO"]
上の例のロジックをもし関数に翻訳するとしたら次のような意味になる。
function UppercaseRepeat(T: string, Repeat: 1 | 2 | 3): string[] {
const Text = Uppercase(T);
return Repeat === 1
? [Text]
: Repeat === 2
? [Text, Text]
: Repeat === 3
? [Text, Text, Text]
: (null as never);
}
型引数を使った方法
infer
を使う以外にも、型引数を使ってローカル変数を作る方法も考えられる。
type UppercaseRepeat<
T extends string,
Repeat extends 1 | 2 | 3,
Text extends Uppercase<T> = Uppercase<T> // ローカル変数
> = Repeat extends 1
? [Text]
: Repeat extends 2
? [Text, Text]
: Repeat extends 3
? [Text, Text, Text]
: never;
再帰呼び出し回数の上限突破
型レベルの再帰呼び出しには上限があり、40回+でエラーになってしまう:
type Split<
String extends string,
Chars extends string[] = []
> = String extends `${infer Char}${infer Rest}`
? Split<Rest, [...Chars, Char]>
: Chars;
type T1 = Split<"abc">;
//=> ["a", "b", "c"]
type T2 = Split<"12345678901234567890123456789012345678901234567">;
//=> ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", ... 13 more ..., "7"]
type T3 = Split<"123456789012345678901234567890123456789012345678">;
// Error: Type instantiation is excessively deep and possibly infinite.(2589)
回避策として、オブジェクトのプロパティだったら回数制限がないらしいので、再帰処理はオブジェクトに包んであげると良い
type Split2<
String extends string,
Chars extends string[] = []
> = String extends `${infer Char}${infer Rest}`
? { x: Split2<Rest, [...Chars, Char]> }
: Chars;
type T4 = Split2<"abc">;
//=> {
// x: {
// x: {
// x: ["a", "b", "c"];
// };
// };
// }
あとは、包んだ値を取り出すユーティリティー型を作って:
type Unwrap<T> = T extends { x: never }
? never
: T extends { x: { x: { x: infer U } } }
? { x: Unwrap<U> }
: T extends { x: { x: infer U } }
? { x: Unwrap<U> }
: T extends { x: infer U }
? U
: T;
type UnwrapRerursive<T> = T extends { x: unknown } ? UnwrapRerursive<Unwrap<T>> : T;
組み合わせて使う:
type T5 = UnwrapRerursive<Split2<"abc">>;
//=> ["a", "b", "c"]
これで再帰可能数が圧倒的に上昇する:
type T7 = UnwrapRerursive<Split2<"1234567890123456789012345678901234567890123456789123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890">>;
//=> ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "1", "2", "3", ... 105 more ..., "0"]
参考文献
汎用的なモジュールにした。
export declare const deepSafe: unique symbol;
export type UnwrapDeepSafe<T> = T extends { [deepSafe]: unknown }
? UnwrapDeepSafe<ReduceDepth<T>>
: T;
type ReduceDepth<T> =
//
T extends { [deepSafe]: never }
? never
: // 3 to 1
T extends {
[deepSafe]: {
[deepSafe]: {
[deepSafe]: infer U;
};
};
}
? { [deepSafe]: ReduceDepth<U> }
: // 2 to 1
T extends {
[deepSafe]: {
[deepSafe]: infer U;
};
}
? { [deepSafe]: ReduceDepth<U> }
: // 1 to 0
T extends {
[deepSafe]: infer U;
}
? U
: T;
Union型からタプル型を作る
type UnionToTuple<
Union,
Items extends any[] = []
> = UnionToOverloadFunction<Union> extends infer OverloadFunction
? ParameterOfLastOverloadFunction<OverloadFunction> extends infer Item
? Exclude<Union, Item> extends infer RestUnion
? [RestUnion] extends [never]
? [Item, ...Items]
: UnionToTuple<RestUnion, [Item, ...Items]>
: never
: never
: never;
// prettier-ignore
type UnionToOverloadFunction<Union> =
(
Union extends any
? (hof: (u: Union) => any) => any
: never
) extends (f: infer HOF) => any
? HOF
: never;
type ParameterOfLastOverloadFunction<OverloadFunction> =
OverloadFunction extends (arg: infer Arg) => any ? Arg : never;
type Tuple = UnionToTuple<1 | 2 | 3 | 4 | 5 | 6 | 7>;
//=> [1, 2, 3, 4, 5, 6, 7]
- ユニオン型からタプル型に変形する直接的な方法がないので、一度関数を経由する必要ある。
- おおまかな処理の流れ:
-
A | B | C
をオーバーロード関数(a: A) => any & (b: B) => any & (c: C) => any
にする。 -
オーバーロード関数のパラメーターを取ろうとすると、最後の関数のパラメーターが取れるという特性を利用して、パラメーターを一つずつ拾っていく。
C
→B
→A
の順で採取される。 - 拾うごとにタプル型の先頭に付け足していく。
[C]
→[B, C]
→[A, B, C]
のようにタプル要素が増えていく。 - パラメーターを取り尽くしたらタプルを返す。
-
never
型かどうか判別する
never
型かどうかを判別するユーティリティー型を作ろうとした。
上手く行かない実装
type IsNever<T> = T extends never ? true : false;
type Test1 = IsNever<string>;
//=> false
type Test2 = IsNever<never>;
//=> never
Test2
はtrue
になってほしいがnever
が返ってきた。
うまくいく実装
タプルにくるんでやると期待通り動作する。
type IsNever<T> = [T] extends [never] ? true : false;
type Test1 = IsNever<string>;
//=> false
type Test2 = IsNever<never>;
//=> true
任意の文字列に出くわすまでの文字列を抽出する
任意の文字列が最初に出現する位置までの文字列と、そのあとに続く文字列を分けて抽出する方法です。
実装が込み入ってるので先に使い方を示す:
type T1 = TakeUntil<"alice@example.com", ["@"]>;
//=> {
// taken: "alice";
// until: "@";
// rest: "example.com";
// }
type T2 = TakeUntil<"foo/bar/buz", ["/"]>;
//=> {
// taken: "foo";
// until: "/";
// rest: "bar/buz";
// }
type T3 = TakeUntil<"1 + 2 - 3 * 4 / 5", ["+", "-", "*", "/"]>;
//=> {
// taken: "1 ";
// until: "+";
// rest: " 2 - 3 * 4 / 5";
// }
type Taken = T3["taken"];
//=> "1 "
type Rest = T3["rest"];
//=> " 2 - 3 * 4 / 5"
実装:
type TakeUntil<
Value extends string,
Until extends [string, ...string[]]
> = UnwrapRecursiveAll<TakeUntil_Recursive<Value, Until>>;
type TakeUntil_Recursive<
Value extends string,
Until extends [string, ...string[]]
> = Until extends [`${infer Until}`, ...infer RestUntil]
? Value extends `${infer Taken}${Until}${infer Rest}`
? { taken: Taken; until: Until; rest: Rest }
: RestUntil extends [string, ...string[]]
? { recursiveSafe: TakeUntil_Recursive<Value, RestUntil> }
: never
: never;
type ReduceRecursiveSafe<T> = T extends { recursiveSafe: never }
? never
: T extends { recursiveSafe: { recursiveSafe: { recursiveSafe: infer U } } }
? { recursiveSafe: ReduceRecursiveSafe<U> }
: T extends { recursiveSafe: { recursiveSafe: infer U } }
? { recursiveSafe: ReduceRecursiveSafe<U> }
: T extends { recursiveSafe: infer U }
? U
: T;
type UnwrapRecursiveAll<T> = T extends { recursiveSafe: unknown }
? UnwrapRecursiveAll<ReduceRecursiveSafe<T>>
: T;
文字列タプルを任意の文字列で結合する方法
型レベルでString.join()
的なことをする方法です。
// prettier-ignore
type StringJoin<Strings extends string[], Glue extends string> =
Strings extends [`${infer String}`, ...infer RestStrings]
? RestStrings extends [string, ...string[]]
? `${String}${Glue}${StringJoin<RestStrings, Glue>}`
: `${String}` // the last string of the given Strings
: ""; // zero element Strings
type T1 = StringJoin<[], ",">;
//=> ""
type T2 = StringJoin<["a"], ",">;
//=> "a"
type T3 = StringJoin<["a", "b"], ",">;
//=> "a,b"
type T4 = StringJoin<["a", "b", "c"], ",">;
//=> "a,b,c"
コールスタックを見る方法
型レベルでも再帰呼び出しを書くことあり、何かトラブルが発生したとき、どこが原因かデバッグしづらい課題があります。
そういったときにコールスタックが見れると便利です。
型レベルでコールスタックを見れるようにする方法を紹介します。
やりかた
例えば、次のようなデバッグ対象があるとします。まだコールスタックが見れない状態のコードです。
type CenterOf<
T extends any[]
> =
T extends []
? never
: T extends [any] | [any, any]
? T[0]
: T extends [any, ...infer Mid, any]
? Mid extends [any] | [any, any]
? Mid[0]
: CenterOf<Mid>
: never;
type T1 = CenterOf<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]>;
//=> 8
コールスタックを記録する型変数を追加します。また、再帰呼び出しするところでは、_CurrentStack
を受け渡すようにします。そして、終了条件のところでは、& {stack: _CurrentStack}
を付与して型を返すようにします。
+type Stack<Frames extends any[] = any[]> = Frames;
type CenterOf<
T extends any[],
+ _PreviousStack extends Stack = [],
+ _CurrentStack extends Stack = [["CenterOf", T], ..._PreviousStack]
> =
T extends []
? never
: T extends [any] | [any, any]
? T[0]
: T extends [any, ...infer Mid, any]
? Mid extends [any] | [any, any]
- ? Mid[0]
- : CenterOf<Mid>
+ ? Mid[0] & { stack: _CurrentStack }
+ : CenterOf<Mid, _CurrentStack>
: never;
これによりコールスタックが見れるようになります:
type Stack<Frames extends any[] = any[]> = Frames;
// prettier-ignore
type CenterOf<
T extends any[],
_PreviousStack extends Stack = [],
_CurrentStack extends Stack = [["CenterOf", T], ..._PreviousStack]
> =
T extends []
? never
: T extends [any] | [any, any]
? T[0]
: T extends [any, ...infer Mid, any]
? Mid extends [any] | [any, any]
? Mid[0] & { stack: _CurrentStack }
: CenterOf<Mid, _CurrentStack>
: never;
type T1 = CenterOf<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]>;
type T1Stack = T1["stack"];
//=> [
// ["CenterOf", [7, 8, 9]],
// ["CenterOf", [6, 7, 8, 9, 10]],
// ["CenterOf", [5, 6, 7, 8, 9, 10, 11]],
// ["CenterOf", [4, 5, 6, 7, 8, 9, 10, 11, 12]],
// ["CenterOf", [3, ... 9 more ..., 13]],
// [...],
// [...],
// ]
ユニオン型が多すぎると破綻する
IPv4のアドレスにマッチする型を作ろうとした
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
type _1_9 = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
type _10_99 = `${_1_9}${Digit}`;
type _100_199 = `1${Digit}${Digit}`;
type _200_249 = `2${"0" | "1" | "2" | "3" | "4"}${Digit}`;
type _250_255 = `25${"0" | "1" | "2" | "3" | "4" | "5"}`;
type EightBit = _1_9 | _10_99 | _100_199 | _200_249 | _250_255;
type IPv4 = `${EightBit}.${EightBit}.${EightBit}.${EightBit}`;
${EightBit}.${EightBit}.${EightBit}.${EightBit}
のところで、Expression produces a union type that is too complex to represent.(2590)というエラーが出てしまった。256^4個のユニオン型を扱うのは無理っぽい
型レベルでIPv4アドレスをバリデーションする
文字列リテラル型がIPv4アドレスの形式になっているかチェックする方法です。
type _0_9 = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
type _10_99 = `${"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"}${_0_9}`;
type _100_199 = `1${_0_9}${_0_9}`;
type _200_249 = `2${"0" | "1" | "2" | "3" | "4"}${_0_9}`;
type _250_255 = `25${"0" | "1" | "2" | "3" | "4" | "5"}`;
type _0_255 = _0_9 | _10_99 | _100_199 | _200_249 | _250_255;
type IPv4<T extends string> = string extends T
? Err<"`T` must be a string literal type, but a generic string type was given">
: Split<T, "."> extends [infer Part1, infer Part2, infer Part3, infer Part4]
? [Part1, Part2, Part3, Part4] extends [_0_255, _0_255, _0_255, _0_255]
? T
: Err<`Invalid IPv4 address: ${T}`>
: Err<`Invalid IPv4 address: ${T}`>;
interface Err<T> {
error: T;
}
type Split<String extends string, Glue extends string> = string extends String
? [] & {
error: "`String` must be a string literal type, but a generic string type was given";
}
: string extends Glue
? [] & {
error: "`Glue` must be a string literal type, but a generic string type was given";
}
: String extends `${infer Chunk}.${infer Rest}`
? [Chunk, ...Split<Rest, Glue>]
: [String];
function ipv4<T extends string>(address: IPv4<T>): IPv4<T> {
return address;
}
// valid
ipv4("0.0.0.0");
ipv4("1.1.1.1");
ipv4("255.255.255.255");
// invalid
ipv4("foo.bar.buz.hoge");
ipv4("1.1.1");
ipv4("256.0.0.0");
タプル型の最後の要素を取り出す
type Last<T extends any[]> = T extends [...any, infer Last] ? Last : never;
type Test1 = Last<[]>;
//=> never
type Test2 = Last<[1]>;
//=> 1
type Test3 = Last<[1, 2]>;
//=> 2
type Test4 = Last<[1, 2, 3]>;
//=> 3
type Test5 = Last<number[]>;
//=> never
再帰回数上限と型特定不能のしきい値
次のような再帰型があります。
type Repeat<Max extends number, Counter extends any[] = []> =
Max extends Counter['length']
? Max
: Repeat<Max, [...Counter, any]>;
ちなみにこの型は意味的な次のような関数と同じロジックで、Max
回の再帰呼び出しをするものです。
function Repeat(Max: number, Counter: any[] = []): number {
return Max === Counter.length
? Max
: Repeat(Max, [...Counter, undefined]);
}
再帰型には50回程度までにしか再帰できないという制限があります。
この例でいくと、45回までなら型エイリアスを定義できます。
type Number45 = Repeat<45>;
しかし、46回を超えると型エイリアス定義でコンパイルエラーになります。
ところが、Repeat<46>
が46
であるということはコンパイラは分かっていて、46以外を代入するコードはコンパイルエラーにしてくれます。
ここから分かることは、再帰回数制限に達したからといって、ただちに型の特定が不能になるかというとそうではないようです。
つまり、再帰回数上限と型の特定を諦めるしきい値は別のようです。
ちなみにこの例でいくと、48からは型が特定できずany
になりました。
再帰制限の回避はタプル型でも可能
再帰型には再帰呼び出し回数が50回程度に制約されていて、次のような再帰回数が多くなるケースではコンパイルエラーになります。
type Repeat<Max extends number, Counter extends any[] = []> =
Max extends Counter['length']
? Max
: Repeat<Max, [...Counter, any]>;
type Test1 = Repeat<46>; //=> Type instantiation is excessively deep and possibly infinite.(2589)
再帰制限の回避する裏技として、再帰呼び出しをプロパティにするというものがあると以前に紹介しました。
type Repeat_DeepSafe1<Max extends number, Counter extends any[] = []> =
Max extends Counter['length']
? Max
: { x: Repeat_DeepSafe1<Max, [...Counter, any]> };
type Test1 = Repeat_DeepSafe1<100>; // エラーにならない
ちなみに、ネストした型から目的の値を取り出す方法は次のようにします。
type Unwrap<T> = T extends { x: never }
? never
: T extends { x: { x: { x: infer U } } }
? { x: Unwrap<U> }
: T extends { x: { x: infer U } }
? { x: Unwrap<U> }
: T extends { x: infer U }
? U
: T;
type UnwrapRerursive<T> = T extends { x: unknown } ? UnwrapRerursive<Unwrap<T>> : T;
type Test2 = UnwrapRerursive<Repeat_DeepSafe1<100>>; //=> 100
その後調査したところ、再帰制限の回避はタプル型でもいけることが判明しました。
type Repeat_DeepSafe2<Max extends number, Counter extends any[] = []> =
Max extends Counter['length']
? Max
: [Repeat_DeepSafe2<Max, [...Counter, any]>]; // タプル
type Test3 = Repeat_DeepSafe2<100>; // OK
戻り型はタプルで幾重にも包み込まれるのは、プロパティを用いた場合と同じなので、くるまれた型を取り出す次のようなユーティリティは必要です。
type Unwrap<T> = T extends [never]
? never
: T extends [[[infer U]]]
? [Unwrap<U>]
: T extends [[infer U]]
? [Unwrap<U>]
: T extends [infer U]
? U
: T;
type UnwrapRerursive<T> = T extends [any] ? UnwrapRerursive<Unwrap<T>> : T;
type Test3 = UnwrapRerursive<Repeat_DeepSafe2<100>>; //=> 100
再帰回数制限と型引数の関係
再帰回数制限は、再帰呼び出しをプロパティにすると回避できることは上で説明しました。
では、オブジェクト型に包めばなんでもいいのか、もっというと、くるむロジックをユーティリティ型にしても大丈夫なのか、という疑問が生じました。
例えば、次のようなWrap<T>
を考えました。
type Wrap<T> = { x: T }
これを用いた再帰型を書きました:
type Repeat_DeepSafe3<Max extends number, Counter extends any[] = []> =
Max extends Counter['length']
? Max
: Wrap<Repeat_DeepSafe3<Max, [...Counter, any]>>;
type Wrap<T> = { x: T };
この再帰型を試してみると、再帰回数制限にひっかかりました:
type Test4 = Repeat_DeepSafe3<100>; //=> Type instantiation is excessively deep and possibly infinite.(2589)
じゃあ、くるむロジックをユーティリティ型にできないのか模索したところ、ユーティリティ型の引数をオブジェクト型やタプル型にすればいいことを発見しました。
type Repeat_DeepSafe4<Max extends number, Counter extends any[] = []> =
Max extends Counter['length']
? Max
- : Wrap<Repeat_DeepSafe4<Max, [...Counter, any]>>;
+ : Wrap<[Repeat_DeepSafe4<Max, [...Counter, any]>]>;
-type Wrap<T> = { x: T };
+type Wrap<T extends [any]> = { x: T[0] };
複雑化した型計算結果を単略化するユーティリティ型
型レベルの計算を重ねていくと、導出される型が複雑な表現になり、結論としてどんな型になっているのかわかりにくくなる。
たとえば、Omit
やインターセクション、ユニオンをくみあわせた次のような型は、
type ComplexType =
| (Omit<
Omit<{ foo: number; bar: number; buz?: number }, "foo"> &
Required<{ hoge?: string }>,
"bar"
> &
Readonly<{ piyo: boolean }> & { a: 1 })
| { b: any };
IDEで型の計算結果を見ても、上の式がそのまま表示されるだけで、何がなんだかぱっとは分からない。
こういった場合、次のようなユーティリティ型をつくり、それを通してやると型が簡略化される。
type Simplify<T> = T extends any ? { [P in keyof T]: T[P] } : never;