TypeScript 型ユーティリティ集
ほぼ私のライブラリ ts-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>("~=")
はA
がB
の部分型かつB
がA
の部分型であるときに型エラーにならず、そうでないときに型エラーになります。"="
の内部実装の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;
A
や B
に true
, false
の他に boolean
や never
, any
などが入ってくる可能性もあるため、 TypeEq
で厳密一致するかどうかをチェックする実装にしています。 true
か false
になっていなければすべて 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;
まず与えられた型 U
が never
であれば false
を返します。
次に union distribution[2:1] を用いて U
の各要素 K
取り出し、その K
と U
が等しければ、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}'
は K
が ToNumber
の制約を満たしているというヒントを型システムに与えるために追加していますが、 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
の再帰回数は MakeTuple
の再帰回数は
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>
>;
Index
と Exclude
を組み合わせるだけで実装できます。
ソースコード
使用例(ユニットテスト)
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 で差し替えています。
ソースコード
使用例(ユニットテスト)
Discussion