概要
type-challengesを解き、考える上でポイントだった部分を書き残していきます。
easy編をすべて解けばTypeScriptの基本的な構文については理解したと言えるレベルになれると思います。
easy編はたった14問なのでTypeScriptを勉強している方はチャレンジすると良いと思います。
type-challengesとは
https://github.com/type-challenges/type-challenges
上記リポジトリにある、TypeScriptの型定義に関する問題集です。
全部で150問あり、easy、medium、hard、extremeの4レベルに分けられています。
日本語の問題もあり、解答例もgithubのissueで利用することができるます。
easyもそこそこ難しく、TypeScriptの型定義で使われる文法すべてを最低限知っておく必要があります。
TypeScriptの学習に関してはMicrosoftのTypeScript Handbookが充実しています。(一通りこちらのドキュメントを読んでから問題に取り組むことを推奨します。)
環境構築
- pnpmのインストール
- pnpm install && pnpm generate
- 追加ライブラリのインストール(typescriptを入れてtscコマンドを使えるようにすると、テストケースをパスしたかチェックできます)
- eslintとprettierの設定
各問題の解説
0004
Indexed Access TypesとMapped Typesを理解できれば解けるはずです。
[k in K]
でK
としてユニオン型を使えばそのユニオン型に含まれるkeyを繰り返す動きになります。(Indexed Access Types)
T[k]
でオブジェクト型T
のk
フィールドの型を取り出すことができます。(Mapped Access Types)
genericsでKの型をTのkeyに制限してやる(K extends keyof T
)ことのがポイントです。(そうしないとエラーケースをパスしません。)
type MyPick<T, K extends keyof T> = {
[k in K]: T[k]
}
00007
オブジェクト型のプロパティの前にreadonly
を付けるとそのプロパティをreadonly型にできることを使います。
Handbookのこのあたりを理解すれば解けます。
Mapping Modiffiersを使えばオブジェクトのプロパティにはreadonly
か否か、optionalか否かの属性を付与/剥奪することができます。
// readonly付与
type MakeReadonly<Type> = {
readonly [Property in keyof Type]: Type[Property];
};
// readonly剥奪
type MakeUnReadonly<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
// optional付与
type MakeOptional<Type> = {
[Property in keyof Type]?: Type[Property];
};
// optional剥奪
type MakeUnOptional<Type> = {
readonly [Property in keyof Type]-?: Type[Property];
};
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
00011
ここまでの問題を理解できていれば解けるはずです。
type TupleToObject<T extends readonly (string | number | symbol)[]> = {
[k in T[number]]: k;
};
00014
[]
で要素数0の配列の型を表現できるのがポイント。要素数が0か否かで場合分けして返す。
type First<T extends any[]> = T[0];
Expect<Equal<First<[]>, never>> // undefinedになってしまう
type First<T extends any[]> = T[0] extends undefined ? never : T[0];
Expect<Equal<First<[undefined]>, undefined>> // neverになってしまう
type First<T extends any[]> = T extends [] ? never : T[0];
00018
Indexed Access Typesでlength
プロパティを取得すれば良い。genericsで受け取る型が何らかの配列であるという制限をするために、extends
で配列に限定することがポイント
type Length<T> = T['length'];
// @ts-expect-error
Length<5> // errorがあがらない
genericsの入力型に配列であるという条件をつけてやればよい
type Length<T extends readonly any[]> = T['length'];
00043
Uであればneverを、UでなければTを返せばOK
type MyExclude<T, U> = T extends U ? never : T;
00189
Promiseが自分自身を内包し得る型であるため、再帰的な型定義を考えたいです。ジェネリクスで受け取った型TがPromiseならばその中身の型Uに対して再帰的に自身を呼び出し、Non-Promiseならばそのまま返すような型定義RAwaitedを作るのがポイントです。
このままだとNon-Promiseをそのまま渡される場合のエラーケースに対応できないのでMyAwaitedの中でRwaitedを使うようにし、MyAwaitedでは条件型を使ってエラーを起こすようにしてやればよいです。
実際テストケースにPromise<Promise<Promise<T>>>
みたいなパターンも含まれているのでこれに対応する必要があります。
type RAwaited<T> = T extends PromiseLike<infer U> ? RAwaited<U> : T;
type MyAwaited<T extends PromiseLike<any>> = RAwaited<T>;
00268
true型、false型を使って条件分岐するだけ
type If<C extends boolean, T, F> = C extends true ? T : F;
00533
型におけるスプレッド構文を使うことがポイント
参考:https://zenn.dev/cookiegg/articles/typescript-spread-type
(1 | 2)[]
と[1, 2]
の型は違うことを理解する必要がある
// 以下だとConcat<[1, 2], []>の結果が(1 | 2)[]となってしまうのでNG
type Concat<T extends any[], U extends any[]> = (T[number] | U[number])[];
type Concat<T extends any[], U extends any[]> = [...T, ...U];
00898
easyレベルの問題の中で最高難易度の問題。
ポイントは2つ
- 型の一致判定をどうするか?
- 配列型の一つ一つの要素の型との一致比較をどうするか
1についてはextendsは使えない。extendsはあくまでextends(拡張)した型であることを判定するものであるため。
そこで少しずるいが、type-challengesのテストケースを見るとEqual<A,B>
なる型が用意されてあり、これを利用すれば型の一致判定を行うことができる。もちろん、自分で同じような型を実装するのもあり。type-challengesのEqual
型の実装を見て理解するのも勉強になると思うのでぜひ。
2については00189でやったような再帰的な型定義を使うことで解決できる。
上記ポイントに加えextends構文をきっちり理解し、inferを使えるかもポイント。extends構文とinferの使い方についてはこちらの記事が分かりやすかった。TypeScriptのextendsってなんなん?
またタプル型の残余展開を利用し、タプル型の要素を1番目とそれ以外に分ける方法も知っておく必要がある。残余展開についてはHandbook
参照
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;
type Includes<T extends readonly any[], U> = T extends [infer T1, ...infer Ts]
? Equal<T1, U> extends true
? true
: Includes<Ts, U>
: false;
03057
簡単。タプル型の残余展開を知っていれば解けるはず。
type Push<T extends any[], U> = [...T, U];
03060
簡単。03057と同様。
type Unshift<T extends unknown[], U> = [U, ...T];
03312
関数Tに何個存在するか分からない引数をまとめてinferする方法がわかれば解ける。正解を言ってしまうと、適当に引数に名前をつけて(正解例ではarg
)、その引数を残余展開すればよい。...
を付けるのが型名ではなく変数名の前であることがポイントで、これによりTは引数の方の配列になる。
type MyParameters<T> = T extends (...args: infer I) => any ? I : never;