Open21

TypeScript型レベルプログラミングの細かいテクニック

ピン留めされたアイテム
suinsuin

TypeScriptで込み入った型レベルプログラミングをする上で、もしかしたら役に立つかもしれないニッチなテクニックを雑にメモする場所です。

実案件で役立つものというより、型レベルですごい魔法なようなことをするライブラリを作るときに使うようなものを中心に載せています。

suinsuin

長いタプル型も短いタプル型に代入できない

2項タプルは1項タプルに代入できても良さそうだが、できないようになっている。

const tuple2: [any, any] = [null, null];
const tuple1: [any] = tuple2;

代入不可の理由

  1. lengthが互換していないため

  1. sort, fill, copyWithinメソッドが互換していないため

ただし、readonlyなタプルではsortなどがないため、これらの互換性は関係なくなる。

type Tuple1 = readonly [any];
type Tuple2 = readonly [any, any];
suinsuin

型レベルのテストで型が一致するかテストする方法

次のような関数を定義して、比較する型を型引数に与える。一致を期待する場合は、関数の引数trueを、不一致を期待する場合はfalseを与える。

declare function assertSame<A, B>(
  expect: [A] extends [B] ? ([B] extends [A] ? true : false) : false
): void;

使い方:

suinsuin

型レベルの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を覗くと、デバッグに使える情報が見れる

suinsuin

交差型のオブジェクトを合体させて、単一のオブジェクト型にする

{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したあとは、ちょっと読みやすくなる

suinsuin

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;
};

suinsuin

型レベルでローカル変数っぽいことをする

型変数にもローカル変数が欲しくなることがある。

ローカル変数の構文はないが、次のような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;
suinsuin

再帰呼び出し回数の上限突破

型レベルの再帰呼び出しには上限があり、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"]

参考文献

suinsuin

汎用的なモジュールにした。

deepSafe.ts
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;
suinsuin

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]
  • ユニオン型からタプル型に変形する直接的な方法がないので、一度関数を経由する必要ある。
  • おおまかな処理の流れ:
suinsuin

never型かどうか判別する

never型かどうかを判別するユーティリティー型を作ろうとした。

上手く行かない実装

type IsNever<T> = T extends never ? true : false;
type Test1 = IsNever<string>;
//=> false
type Test2 = IsNever<never>;
//=> never

Test2trueになってほしいがneverが返ってきた。

うまくいく実装

タプルにくるんでやると期待通り動作する。

type IsNever<T> = [T] extends [never] ? true : false;
type Test1 = IsNever<string>;
//=> false
type Test2 = IsNever<never>;
//=> true
suinsuin

任意の文字列に出くわすまでの文字列を抽出する

任意の文字列が最初に出現する位置までの文字列と、そのあとに続く文字列を分けて抽出する方法です。

実装が込み入ってるので先に使い方を示す:

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;
suinsuin

文字列タプルを任意の文字列で結合する方法

型レベルで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"
suinsuin

コールスタックを見る方法

型レベルでも再帰呼び出しを書くことあり、何かトラブルが発生したとき、どこが原因かデバッグしづらい課題があります。

そういったときにコールスタックが見れると便利です。

型レベルでコールスタックを見れるようにする方法を紹介します。

やりかた

例えば、次のようなデバッグ対象があるとします。まだコールスタックが見れない状態のコードです。

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]],
//   [...],
//   [...],
// ]
suinsuin

ユニオン型が多すぎると破綻する

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個のユニオン型を扱うのは無理っぽい

suinsuin

型レベルで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");

suinsuin

タプル型の最後の要素を取り出す

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
suinsuin

再帰回数上限と型特定不能のしきい値

次のような再帰型があります。

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になりました。

suinsuin

再帰制限の回避はタプル型でも可能

再帰型には再帰呼び出し回数が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
suinsuin

再帰回数制限と型引数の関係

再帰回数制限は、再帰呼び出しをプロパティにすると回避できることは上で説明しました。

では、オブジェクト型に包めばなんでもいいのか、もっというと、くるむロジックをユーティリティ型にしても大丈夫なのか、という疑問が生じました。

例えば、次のような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] };
suinsuin

複雑化した型計算結果を単略化するユーティリティ型

型レベルの計算を重ねていくと、導出される型が複雑な表現になり、結論としてどんな型になっているのかわかりにくくなる。

たとえば、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;