Argano の松田です。この記事では、TypeScript のスキルアップに役立つ Type Challenges について紹介します。
Type Challenges とは
Type Challenges は、TypeScript の型システムだけを使った「型パズル」の問題集で、GitHub 上で公開されています。 様々な難易度の問題が用意されており、実際に手を動かして型定義を考えることで、TypeScript の理解を深めることができます。
Github: https://github.com/type-challenges/type-challenges
問題文は日本語に翻訳されたものも存在するため、英語が苦手な方でも取り組みやすいかと思います。
難易度について
難易度は以下のように分かれています。
- お試し (Warm-up) : 解き方の例が用意されているチュートリアル枠
- 初級・中級 : 初見だと意外と難しく、実務で役立つテクニックが多い
- 上級・最上級 : TS の型システムの限界に挑むような、深い知識が求められる問題
また、正解は一つではありません。Issue には世界中のエンジニアから様々な解法が投稿されているため、他の人のコードを見ることで新たな発見があります。
全問解いてみて感じたメリット
筆者が実際に取り組んでみて、以下のようなメリットがありました。
- TypeScript への理解が数段深まった
- ライブラリ内部の複雑な型を読むことへの抵抗がなくなった
- 型レベルプログラミングの基礎が身についた
- 単純にパズルとして楽しい
初級問題例
https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ja.md
組み込みの型ユーティリティ
Pick<T, K>を使用せず、TからKのプロパティを抽出する型を実装します。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = MyPick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
上記の問題では、MyPick という型を実装する必要があります。MyPick は、与えられた型 T から、指定されたキー K のプロパティだけを抽出した新しい型を作成します。
解答例
type MyPick<T, K extends keyof T> = {
[key in K]: T[key];
};
この一問でも、TypeScript の以下のような知識が必要となります。
- ジェネリクス :
<T, K> - ジェネリクスの制約 :
K extends ... keyof演算子 :keyof T- Mapped Types :
{ [key in K]: ... } - インデックスアクセス型 :
T[key]
Type Challenges は、一つの機能だけでなく、複数の機能を組み合わせて解く必要がある問題が多いため、思考力と型システムの理解が同時に鍛えられます。
中級問題例
組み込みの型ユーティリティ ReturnType
を使用せず、T の戻り値の型を取得する型を実装します。
const fn = (v: boolean) => {
if (v) return 1;
else return 2;
};
type a = MyReturnType<typeof fn>; // should be "1 | 2"
上記の問題では、MyReturnType という型を実装する必要があります。MyReturnType は、関数型 T の戻り値の型を抽出します。
解答例
type MyReturnType<T extends Function> = T extends (...args: any) => infer R
? R
: never;
この問題では、以下のような知識が必要となります。
- Conditional Types (条件付き型) :
T extends ... ? ... : ... inferキーワード :infer R- 関数型の表現 :
(...args: any) => ...
Type Challenges の中級問題では、より高度な型システムの機能が求められ、実務で役立つテクニックも多く含まれています。
特に infer キーワードは、型の一部を抽出するために非常に便利であり、型レベルプログラミングにおいて重要な役割を果たします。
上級問題例
必須なフィールドのみを残す高度なユーティリティ型
GetRequired<T>を実装してください。
type I = GetRequired<{ foo: number; bar?: string }>; // expected to be { foo: number }
上記の問題では、GetRequired という型を実装する必要があります。GetRequired は、与えられた型 T から必須プロパティのみを抽出した新しい型を作成します。
解答例
type GetRequired<T> = {
[P in keyof T as T[P] extends Required<T>[P] ? P : never]: T[P];
};
この問題では、以下のような知識が必要となります。
- Key Remapping :
[P in keyof T as ...] - 条件付き型の応用 :
T[P] extends Required<T>[P] ? P : never - 組み込み型ユーティリティ
Required<T> never型
never 型も重要な概念であり、型レベルプログラミングにおいて特定の条件を満たさない場合に使用されます。
最上級問題例
https://github.com/type-challenges/type-challenges/blob/main/questions/00476-extreme-sum/README.md
Implement a type Sum<A, B> that summing two non-negative integers and returns the sum as a string. Numbers can be specified as a string, number, or bigint.
(日本語翻訳されていないため Deepl 翻訳)
非負整数 2 つを足し合わせ、その和を文字列として返す型 Sum<A, B>を実装してください。数値は文字列、数値、または bigint で指定できます。
type T0 = Sum<2, 3>; // '5'
type T1 = Sum<"13", "21">; // '34'
type T2 = Sum<"328", 7>; // '335'
type T3 = Sum<1_000_000_000_000n, "123">; // '1000000000123'
上記の問題では、Sum という型を実装する必要があります。Sum は、与えられた 2 つの非負整数を足し合わせ、その和を文字列として返します。
なんと TypeScript の型システムは、数値の加算まで表現できてしまいます。他にも掛け算を表現する問題などもあります。
最上級問題は名前の通り非常に難易度が高いですが、上級以下の知識と少しのひらめきで解ける問題が多いです。挑戦してみる価値は十分にあるでしょう。
解答例は非常に長いため、ここでは割愛します。興味のある方は GitHub リポジトリをご覧ください。
まとめ
Type Challenges は、最初は難しく感じるかもしれません。特に infer や再帰的な型が出てくると、理解に時間がかかります。
しかし、これを乗り越えると TypeScript が単なる「静的型付けツール」ではなく、「表現力豊かなプログラミング言語」であることに気づきます。
まずは初級問題から始めて、徐々にステップアップしていくことをお勧めします。TypeScript の型システムの奥深さを楽しみながら、スキルアップを目指しましょう!
