🐈

TypeScript 型ユーティリティ集

2023/05/30に公開

ほぼ私のライブラリ ts-type-utils-no-stdlibts-type-utils からの抜粋です(都合により TypeScript 標準ライブラリ(lib.es5.d.ts 等)に依存するかどうかでパッケージを分けています)。
随時更新していきます。


expectType (型のユニットテスト用ユーティリティ)

expectType<[1, 2, 3], [1, 2, 3]>('=');
expectType<[any], [number]>('<=');
expectType<number, string>('!=');
expectType<any, 1>('!=');

型の等価性や部分型関係をチェックするユーティリティです。

  • expectType<A, B>("=") は型 A と型 B が等価であるときに型エラーにならず、そうでないときに型エラーになります。
  • expectType<A, B>("!=")"=" の逆です。
  • expectType<A, B>("<=") は型 A が型 B の部分型であるとき型エラーにならず、部分型でないときに型エラーになります。
  • expectType<A, B>("~=")AB の部分型かつ BA の部分型であるときに型エラーにならず、そうでないときに型エラーになります。 "=" の内部実装の TypeEq<A, B> は例えば { x: 1 } & { y: 2 }{ x: 1; y: 2 } を区別してしまう程厳密なものなので、もう少し緩い等価判定を行いたい場合に用意しています。

--- 実装 ---

/**
 * @param _relation `"=" | "~=" | "<=" | ">=" | "!<=" | "!>=" | "!="`
 * @description
 * - `expectType<A, B>("=")` passes if `A` is equal to `B`.
 * - `expectType<A, B>("~=")` passes if `A` extends `B` and `B` extends `A`.
 * - `expectType<A, B>("<=")` passes if `A` extends `B`.
 * - `expectType<A, B>(">=")` passes if `B` extends `A`.
 * - `expectType<A, B>("!<=")` passes if `A` doesn't extend `B`.
 * - `expectType<A, B>("!>=")` passes if `B` doesn't extend `A`.
 * - `expectType<A, B>("!=")` passes if `A` is not equal to `B`.
 */
export const expectType = <A, B>(
  _relation: TypeEq<A, B> extends true
    ? '<=' | '=' | '>=' | '~='
    :
        | '!='
        | (TypeExtends<A, B> extends true
            ? '<=' | (TypeExtends<B, A> extends true ? '>=' | '~=' : '!>=')
            : '!<=' | (TypeExtends<B, A> extends true ? '>=' : '!>=')),
): void => undefined;
// prettier-ignore
type TypeEq<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2)
    ? true
    : false;

type TypeExtends<A, B> = A extends B ? true : false;
使用例(ユニットテスト)

BoolAnd, BoolOr, BoolNot, BoolEq, BoolNeq, BoolNand, BoolNor,

expectType<BoolAnd<true, true>, true>('=');
expectType<BoolOr<false, true>, true>('=');
expectType<BoolNot<true>, false>('=');
expectType<BoolEq<false, false>, true>('=');
expectType<BoolNeq<false, true>, true>('=');
expectType<BoolNand<false, true>, true>('=');
expectType<BoolNor<false, false>, true>('=');

論理演算を行う関数です。

--- 実装(一部) ---

type BoolAnd<A extends boolean, B extends boolean> =
  TypeEq<A, true> extends true
    ? TypeEq<B, true> extends true
      ? true
      : TypeEq<B, false> extends true
        ? false
        : never
    : TypeEq<A, false> extends true
      ? TypeEq<B, true> extends true
        ? false
        : TypeEq<B, false> extends true
          ? false
          : never
      : never;

ABtrue, false の他に booleannever, any などが入ってくる可能性もあるため、 TypeEq で厳密一致するかどうかをチェックする実装にしています。 truefalse になっていなければすべて never を返します。

ソースコード(残りの実装)
使用例(ユニットテスト)

IsNever

expectType<IsNever<never>, true>('=');
expectType<IsNever<string>, false>('=');

型が never と等しいかどうか判定します。
Type Challenges[1]にも掲載されています。

--- 実装 ---

type IsNever<T> = [T] extends [never] ? true : false;
テスト

IsUnion

expectType<IsUnion<never>, false>('=');
expectType<IsUnion<string>, false>('=');
expectType<IsUnion<number | string>, true>('=');

型が(2 個以上の型の) union 型かどうか判定します。
Type Challenges[3]にも掲載されています。

type IsUnion<U> = _IsUnionImpl<U, U>;

/** @internal */
type _IsUnionImpl<U, K extends U> =
  IsNever<U> extends true ? false : K extends K ? BoolNot<TypeEq<U, K>> : never;

まず与えられた型 Unever であれば false を返します。
次に union distribution[2:1] を用いて U の各要素 K 取り出し、その KU が等しければ、U は 1 要素の union ということになるので false を返し、そうでない場合は true を返す、という仕組みです。なお、最後の never に評価されることはありません(union distribution のイディオム)。

ソースコード
使用例(ユニットテスト)

ToNumber

expectType<ToNumber<'1000'>, 1000>('=');
expectType<ToNumber<'8192'>, 8192>('=');

数値の文字列型を数値型にします。

--- 実装 ---

// prettier-ignore
type ToNumber<S extends `${number}`>
  = S extends `${infer N extends number}` ? N : never;
ソースコード
使用例(ユニットテスト)

IsFixedLengthList

expectType<IsFixedLengthList<readonly [1, 2, 3]>, true>('=');
expectType<IsFixedLengthList<readonly number[]>, false>('=');
expectType<IsFixedLengthList<[number, 1, 2, ...number[]]>, false>('=');

配列型が固定長であるかどうかを返します。

--- 実装 ---

type IsFixedLengthList<T extends readonly unknown[]> =
  number extends T['length'] ? false : true;

可変長配列( readonly number[] など)の"length" の型が number 型であるのに対して、固定長の配列型(タプル型、 [1, 2, 3] など)の "length" の型が number 型ではなく定数の型(3など)になることを利用しています。

ソースコード
使用例(ユニットテスト)

IndexOfTuple

expectType<IndexOfTuple<readonly [1, 2, 3]>, 0 | 1 | 2>('=');
expectType<IndexOfTuple<readonly [2, 4, 6, 8, 10]>, 0 | 1 | 2 | 3 | 4>('=');
expectType<IndexOfTuple<readonly []>, never>('=');

タプル型のインデックスを返します。

--- 実装 ---

type IndexOfTuple<T extends readonly unknown[]> = _IndexOfTupleImpl<T, keyof T>;

type _IndexOfTupleImpl<T extends readonly unknown[], K> =
  IsFixedLengthList<T> extends true
    ? K extends keyof T
      ? K extends `${number}`
        ? ToNumber<K>
        : never
      : never
    : number;

タプル型 T から keyof T を取り出してそれらを ToNumber で map した結果を得るという実装です。 K extends keyof T のところで union distribution[2:2] を使っています。
K extends '${number}'KToNumber の制約を満たしているというヒントを型システムに与えるために追加していますが、 IndexOfTuple からの入力では必ず真になるので実質何もしていない条件部です。

ソースコード
使用例(ユニットテスト)

MakeTuple

expectType<MakeTuple<unknown, 3>, readonly [unknown, unknown, unknown]>('=');

第 1 引数の型を第 2 引数の整数個分繰り返した配列を作ります。

--- 実装 ---

type MakeTuple<Elm, N extends number> = _MakeTupleInternals.MakeTupleImpl<
  Elm,
  `${N}`,
  []
>;

namespace _MakeTupleInternals {
  type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

  type Tail<T extends string> = T extends `${Digit}${infer U}` ? U : never;

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

  type DigitStr = `${Digit}`;

  type Tile<
    T extends readonly unknown[],
    N extends Digit | DigitStr | '10' | 10,
  > = [
    readonly [],
    readonly [...T],
    readonly [...T, ...T],
    readonly [...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T, ...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T],
    readonly [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T],
  ][N];

  export type MakeTupleImpl<
    T,
    N extends string,
    X extends readonly unknown[],
  > = string extends N
    ? never
    : N extends ''
      ? X
      : First<N> extends infer U
        ? U extends DigitStr
          ? MakeTupleImpl<
              T,
              Tail<N>,
              readonly [...Tile<[T], U>, ...Tile<X, 10>]
            >
          : never
        : never;
}

かなり大がかりですが、巨大な tuple 型を作ろうとしても再帰制限にひっかからないようにするためこのように実装が工夫がされています。以下の記事で紹介されていたものをほぼそのまま利用しました(ToNumber の実装だけ TypeScript 4.8 の機能を使い効率化しています)。

参考: https://techracho.bpsinc.jp/yoshi/2020_09_04/97108

以下の単純な再帰を行う実装でも小さな N に対しては同様に動きますが、 N が大きい場合に再帰回数の制限にひっかかってしまいます。
MakeTupleNaive の再帰回数は O(N) なのに対し、 MakeTuple の再帰回数は O(\log_{10} N) になります。

type MakeTupleNaive<Elm, N extends number> = _MakeTupleNaiveImpl<
  N,
  Elm,
  readonly []
>;

/** @internal */
type _MakeTupleNaiveImpl<Num, Elm, T extends readonly unknown[]> =
  //
  T extends { length: Num }
    ? T
    : _MakeTupleNaiveImpl<Num, Elm, readonly [Elm, ...T]>;
expectType<MakeTupleNaive<0, 1000>, MakeTuple<0, 1000>>('=');
// Type instantiation is excessively deep and possibly infinite. ts(2589)
ソースコード
使用例(ユニットテスト)

Index

expectType<Index<3>, 0 | 1 | 2>('=');
expectType<Index<5>, 0 | 1 | 2 | 3 | 4>('=');

与えられた整数未満の非負整数すべてからなる union 型を返します。

--- 実装 ---

type Index<N extends number> = IndexOfTuple<MakeTuple<0, N>>;

MakeTuple を利用して tuple を作った後 IndexOfTuple でその index を取り出す、という実装をしています。

ソースコード
使用例(ユニットテスト)

NegativeIndex

expectType<NegativeIndex<0>, never>('=');
expectType<NegativeIndex<5>, -1 | -2 | -3 | -4 | -5>('=');

与えられた整数以上の負整数すべて(0 は除く)からなる union 型を返します。

--- 実装 ---

type NegativeIndex<N extends number> = _NegativeIndexImpl.MapIdx<
  RelaxedExclude<Index<N>, 0>
>;

namespace _NegativeIndexImpl {
  type ToNumberFromNegative<S extends `-${number}`> =
    S extends `${infer N extends number}` ? N : never;

  export type MapIdx<I extends number> = I extends I
    ? ToNumberFromNegative<`-${I}`>
    : never;
}

Index と同様 tuple 型の index を取り出す実装を使っていますが、負数にするためにその index I-${I} で文字列化して数値として取り出すという実装をしています。

ソースコード
使用例(ユニットテスト)

Enum types

Index 型を実装したので以下の型も定義しておきます。

/** `0 | 1 | ... | 255` */
type Uint8 = Index<256>;

/** `0 | 1 | ... | 511` */
type Uint9 = Index<512>;

/** `0 | 1 | ... | 1023` */
type Uint10 = Index<1024>;

/** `-128 | -127 | ... | -1 | 0 | 1 | ... | 126 | 127` */
type Int8 = Readonly<Index<128> | NegativeIndex<129>>;

/** `-256 | -255 | ... | -1 | 0 | 1 | ... | 254 | 255` */
type Int9 = Readonly<Index<256> | NegativeIndex<257>>;

/** `-512 | -511 | ... | -1 | 0 | 1 | ... | 510 | 511` */
type Int10 = Readonly<Index<512> | NegativeIndex<513>>;
ソースコード

UintRange

expectType<UintRange<0, 3>, 0 | 1 | 2>('=');
expectType<UintRange<0, 0>, never>('=');
expectType<UintRange<0, 1>, 0>('=');
expectType<UintRange<0, 5>, 0 | 1 | 2 | 3 | 4>('=');
expectType<UintRange<2, 5>, 2 | 3 | 4>('=');

--- 実装 ---

type UintRange<Start extends number, End extends number> = Exclude<
  Index<End>,
  Index<Start>
>;

IndexExclude を組み合わせるだけで実装できます。

ソースコード
使用例(ユニットテスト)

Max, Min

expectType<Max<0 | 1 | 2>, 2>('=');
expectType<Max<0>, 0>('=');
expectType<Max<0 | 1 | 3 | 5 | 6>, 6>('=');

expectType<Min<0 | 1 | 2>, 0>('=');
expectType<Min<0>, 0>('=');
expectType<Min<0 | 1 | 3 | 5 | 6>, 0>('=');

数値の union 型から最大値/最小値を取り出します。

実装はこの記事で解説しています。

ソースコード
使用例(ユニットテスト)

Seq

expectType<Seq<3>, readonly [0, 1, 2]>('=');
expectType<Seq<0>, readonly []>('=');
expectType<Seq<5>, readonly [0, 1, 2, 3, 4]>('=');

与えられた数値までの連番配列の型を返します。

--- 実装 ---

type Seq<N extends number> = _SeqImpl<MakeTuple<unknown, N>>;

type _SeqImpl<T extends readonly unknown[]> = {
  readonly [i in keyof T]: i extends `${number}` ? ToNumber<i> : never;
};

MakeTuple で長さ N の配列を作った後、その中身を Mapped Type で差し替えています。

ソースコード
使用例(ユニットテスト)

脚注
  1. Type Challenges (IsNever) ↩︎

  2. union distribution の説明 https://qiita.com/uhyo/items/da21e2b3c10c8a03952f#mapped-typeのunion-distribution ↩︎ ↩︎ ↩︎

  3. Type Challenges (IsUnion) ↩︎

GitHubで編集を提案

Discussion