【初級】type-challengesの解説(学習記録)
type-challenges は、Github レポジトリにある「TypeScript の型に関する問題集」です。
今記事は初級問題のみを扱います。
Pick
回答
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
}
解説
K extends keyof T
keyof T で type(又はinterface) Tのプロパティ名のUnion型が取得出来ます。
つまり、Kの型は以下になります。
'title' | 'description' | 'completed'
[P in K]: T[P]
Mapped Typeを使います。Mapped Typeとは、あるオブジェクトのプロパティ名を利用して新しい型を作り出す機能です。
今回の例だと、T がTodo型、K が 'title' | 'completed' | 'description' を型とする文字列、P が K を含む文字列要素となります。
T[P] でTodo型の値(型)をキーを用いて参照しています。
Readonly
回答
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
解説
前問のPickと考え方は同じです。今回は読み取り専用のにする問題なので Mapped Type で型を作り直す際に readonly を付けることでプロパティを読み取り専用にしています。
Tuple to Object
回答
type TupleToObject<T extends readonly string[]> = {
[K in T[number]]: K
}
解説
毎度おなじみMapped Typeを使います。ただ今回は型引数がオブジェクトではなくタプルなので Indexed Access Types を利用してArray (string[]) を Union型に変換していきます。
ちなみに、何故 T[number] はUnion型になるのかはこちらの記事が参考になります。
First of Array
回答
type First<T extends any[]> = T[number] extends never ? never : T[0];
or
type First<T extends any[]> = T extends [] ? never : T[0]
解説
通常は T[0] を返却すれば良いのですが空配列の場合は never を返却しなければいけません。ですので、型引数から条件分岐(Conditional Types)をしてあげる必要があります。
Length of Tuple
回答
type Length<T extends readonly string[]> = T['length'];
解説
タプル型の要素数は T['length'] のようにして取得することが出来ます。
何故このようにして要素数が取得できるかは、タプル型は interface として表すことが出来ます。
すなわち、length キーにアクセスすることにより要素数の取得が可能となります。
type Colors = ["white", "red", "black", "purple"]
interface Colors {
length: 4;
0: "white";
1: "red";
2: "black";
3: "purple";
}
Exclude
回答
type MyExclude<T, U> = T extends U ? never : T;
解説
Conditional types には分配則の性質があります。
Union型のConditional Typesは、それぞれのConditional TypesのUnionに展開される性質です。
例えば、Tが 'white' | 'red' | 'purple'
である場合、
T extends U ? never : T = ('white' extends U ? never : 'white') | ('red' extends U ? never : 'red') | ('purple' extends U ? never : 'purple')
となります。
以上より、Unionの各要素が U に含まれている場合 never を返し、含まれていない場合はUnionの各要素を返します。
Awaited
回答
type Awaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
解説
Conditional types と infer を使います。infer はざっくり言うと、「何かの型を持つ配列」や「何かの型を持つプロパティ」等の何かの型を得たい場合に使います。
(こちらの解説が参考になりました。)
// TypeItemListは何の型の配列か
type TypeItemList = Item[];
// Userのプロパティnameは何の型か
type User = {
name: string,
age: number,
}
今回はPromiseの中の型を取り出すので、 Promise<infer U>
で取り出したい型に U の名付け、 Conditional types で返却しています。
(※ infer は Conditional types 上でしか使用することができません。)
If
回答
type If<C extends boolean, T, F> = C extends true ? T : F;
解説
Conditional types の基礎が理解できていれば解ける問題です。
C ? T : F;
とやってしまいそうですが、Cは true
又は false
のリテラル型の型変数なので extends を付けて C が含まれているか見てあげましょう。
Concat
回答
type Concat<T extends any[], U extends any[]> = [...T, ...U];
解説
Variadic Tuple Types を使います。可変長のタプル型を生成することが可能で、スプレッド構文の書き方をすることで実現することが出来ます。
(こちらの記事に詳しく解説があります。)
Includes
回答
type Includes<T extends readonly any[], U extends any> = T extends [
infer First,
...infer Rest
]
? Equal<First, U> extends true
? true
: Includes<Rest, U>
: false;
解説
初級の問題ではないですね・・・
上記回答例は「配列の要素を上から一つ取り出し、指定した型と同じかどうかチェックを繰り返す」という考え方です。
[ infer First, ...infer Rest]
ですが、スプレッド構文の分割代入をして配列の中身を先頭要素とその他に分割しています。
Equal
ですが、 type-challenges/utils
プラグインの機能です。
type-challenges では作成した型だ正しいか評価するためにインポートされているのですが今回は特別に使っています。
(でないと、回答が複雑になる為)
Equal
で配列の先頭要素と第二型引数を比べて正であれば true
、
負であれば Includes
に引数を渡し呼び出すという再帰処理を行っています。
Push
回答
type Push<T extends any[], U> = [...T, U];
解説
前問のConcatと同じ形式です。Concatの問題が理解できていれば難しくありませんでした。
Unshift
回答
type Unshift<T extends any[], U> = [U, ...T];
解説
前問Pushと同じ形式です。
Parameters
回答
type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer U) => any ? U : never;
解説
infer を使います。前問Awaitedでは Promise の中の型を取り出すのに対し、今回は関数の引数の型を取り出します。
考え方は同じで、抽出したい型のところに infer を付けるだけです。
参考
Discussion