2022-05-08
type challenges - warm-up, easy
type-challenges/type-challenges
hello world
type HelloWorld = string;
pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
型引数 K に条件として extends…を書かないといけなかった。
readonly
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
tuple to object
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[P in T[number]]: P;
};
key は string|number|symbol
である必要があるのか。
first of array
type First<T extends unknown[]> = T extends [unknown, ...unknown[]]
? T[0]
: never;
extends で要素がひとつ以上存在することを担保してあげる必要がある。
type First<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
infer をつかってもよい。 infer は extends の右側で導入する。
length of tuple
type Length<T extends readonly unknown[]> = T["length"];
そっか、length はプロパティだから T["length"]
で取り出せるのか。
exclude
type MyExclude<T, U> = T extends U ? never : T;
union distribution って聞いたことあったけど初めて書いた。 extends の左側が型変数で union 型ならばそれは分配される。 never は 0 個の union のように振る舞う。
awaited
MyAwaited<number>
をエラーにするためにはシンプルな再帰では書けないので、まず以下で書いた。
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer U>
? U extends Promise<infer V>
? V
: U
: never;
しかしこれだとネストが 2 階層までに限定されてしまい、例えば以下がとおらない。
type N = Promise<Promise<Promise<string>>>;
Expect<Equal<MyAwaited<N>, string>>;
一番上の階層では型引数に extends を書くことで MyAwaited<number>
とかを弾きつつ、内側では再帰で書けばより一般的に書けた。
上記のテストもとおる。
type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer U>
? MyAwaitedBase<U>
: never;
type MyAwaitedBase<T> = T extends Promise<infer U> ? MyAwaitedBase<U> : T;
if
type If<C extends boolean, T, F> = C extends true ? T : F;
concat
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
配列を展開するみたいに、配列の型も展開できるんだな。
includes
前段として、型の一致を検査することを考える。参考
type Equal_0<X, Y> = X extends Y ? (Y extends X ? true : false) : false;
type _0_true_0 = Equal_0<0, 0>; // true
type _0_false_0 = Equal_0<0, 1>; // false
type _0_true_1 = Equal_0<0 | 1, 0 | 1>; // boolean
これだと union distribution により最後の例で正しく判定できない。
配列にすることで union distribution を回避する。
type Equal_1<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
type _1_true_0 = Equal_1<0, 0>; // true
type _1_false_0 = Equal_1<0, 1>; // false
type _1_true_1 = Equal_1<0 | 1, 0 | 1>; // true
type _1_false_1 = Equal_1<0, 0 | 1>; // false
type _1_false_2 = Equal_1<0, any>; // true
extends は割り当て可能性を判定し、最後の例で 0 と any は互いに割り当て可能なため true を返してしまう。
一応、0 と any が互いに割り当て可能であることを確認しておく。
declare let a: 0;
declare let b: any;
a = b; // エラーにならない
b = a; // エラーにならない
ここで「条件付き型同士が割り当て可能になるには extends 直後の型同士が同値でなければならない」という性質がある。
まずそれを確認する。
declare let foo: <T>() => T extends string ? 0 : 1;
declare let bar: <T>() => T extends any ? 0 : 1;
foo = bar; // エラーになる
bar = foo; // エラーになる
extends を直ちに評価せず残すために、便宜的にジェネリックつきの関数の戻り値の型に extends を用いている。
ちなみにこういう関数型を実際に呼び出してみると、ジェネリックパラメータ T は推論しようがないので unknown となる。
foo(); // let foo: <unknown>() => 1
ちなみに t/f の場合の型が同じだと「extends の右側がなんだろうと結局同じ型でしょ」ということで代入可能になる。
declare let foo: <T>() => T extends string ? 0 : 0;
declare let bar: <T>() => T extends any ? 0 : 0;
foo = bar; // エラーにならない
bar = foo; // エラーにならない
さてこの性質を利用して型の一致を判定する。
type Equal_2<X, Y> = (<T>() => T extends X ? 0 : 1) extends <T>() => T extends Y
? 0
: 1
? true
: false;
type _2_true_0 = Equal_2<0, 0>; // true
type _2_false_0 = Equal_2<0, 1>; // false
type _2_true_1 = Equal_2<0 | 1, 0 | 1>; // true
type _2_false_1 = Equal_2<0, 0 | 1>; // false
type _2_false_2 = Equal_2<0, any>; // false
以上で型の一致の検査をすることができたので、本題の includes に戻る。 一致性をひとつひとつの要素について再帰的に判定していく。
type MyEqual<X, Y> = (<T>() => T extends X ? 0 : 1) extends <T>() => T extends Y
? 0
: 1
? true
: false;
type Includes<T extends readonly unknown[], U> = T extends [infer F, ...infer R]
? MyEqual<F, U> extends true
? true
: Includes<R, U>
: false;
push
type Push<T extends unknown[], U> = [...T, U];
unshift
type Unshift<T extends unknown[], U> = [U, ...T];
parameters
type MyParameters<T extends (...args: any[]) => unknown> = T extends (
...args: infer Args
) => unknown
? Args
: never;