はじめに
この記事では、TypeScript の型の問題集 type-challenges についての簡単な説明を行い、例として数問の解き方と関連する TypeScript の機能を紹介します。
対象読者として、プリミティブ型、オブジェクト、ジェネリクス等、TypeScript の型に関する基礎知識が習得済みの方や、もっと TypeScript の型を使いこなせるようになりたい方を対象とします。
type-challenges とは
TypeScript の型に関する問題集です。以下のリポジトリに問題がまとまっています。
難易度は warm-up
, easy
, medium
, hard
, extreme
となっています。難易度 warm-up
は例題なので、実際は難易度 easy
から取り組むことになると思いますが、難易度 easy
でもそこそこ難しいです。
使い方の詳細は以下の記事が参考になります。
上記の記事で紹介された時よりも、問題数がかなり増えています!
それでは実際に解いてみましょう!
type-challenges に挑戦!
この記事では、例として、難易度 warm-up
を1問と、難易度 easy
を2問、問題とそれを解くために必要な TypeScript の知識・機能を解説します。
【難易度: warm-up】13・Hello World
問題は以下のページから確認できます。
この問題は、 HelloWorld
という名前で、string
と同じになる型を作る問題です。
// expected to be string
type HelloWorld = any
また、以下の test 型で、回答が正しいか確認されます。
// you should make this work
type test = Expect<Equal<HelloWorld, string>>
Take the Challenge
ボタンから、Playground を立ち上げると、テスト用の型がエラーになっていることがわかります。
any
を string
に変えると、エラーがなくなります。
// expected to be string
type HelloWorld = string
このように、type-challenges は特定の条件を満たすような型を作成し、テスト用の型のエラーをなくせば正解です!
この問題は例題なので、さすがに簡単ですね。難易度 easy
の問題を解いてみましょう!
【難易度: easy】7・Readonly
問題は以下のページから確認できます。
ユーティリティ型の Readonly
を自作する、という問題です。
TypeScript には 様々なユーティリティ型が存在し、それを使うことで組み込みのジェネリクス型の変換がしやすくなります。
Readonly
は、オブジェクト型の各プロパティを readonly
にする、というユーティリティ型です。
例として、Readonly<{ x: number }>
は { readonly x: number }
になります。
この型を作るために必要な型の知識として、Mapped Types を紹介します。
Mapped Types は以下のような構文の型です。 K
は型変数、T
S
には何か具体的な型を当てはめます。
{ [K in T]: S }
例として、T
に 'x' | 'y'
、S
に number
を当てはめてみます。
{ [K in 'x' | 'y']: number }
この型は、{ x: number, y: number }
と同じ型になります。
これだけだとインデックス型に似ていますが、インデックス型とは異なり、プロパティのキーを制限できます。
また、それだけでなく、オブジェクトの各プロパティに対し、型の変換ができます。具体的には、readonly
や ?
の付与・除去など、他にもいろいろなことができます。したがって、今回の問題は Mapped Types を使って readonly
を付与すればよさそうですね。
問題形式を見てみましょう。以下のようになっています。
type MyReadonly<T> = any
any
を一旦 readonly
を付与する Mapped Types に置き換えてみます。
以下のように、+readonly
を追加することで、各プロパティは readonly
が付いた型になります。
type MyReadonly<T> = { +readonly [K in XXX]: YYY }
あとは、型引数 T
を使って適切な XXX
と YYY
を作ります。
XXX
にはプロパティのキーの型が必要です。 T
のプロパティのキーの型は、 keyof
を使って、keyof T
とすることで得られます。YYY
は各プロパティの値の型が必要です。型変数の K
を使い、T[K]
と書くことでえられます。(Lookup Types または Indexed Access Types とよばれます。)
以上により、Readonly
を自作することができました!
type MyReadonly<T> = { +readonly [K in keyof T]: T[K] }
もう1問解いてみましょう!
【難易度: easy】4・Pick
問題は以下のページから確認できます。
組み込みのユーティリティ型 Pick
を自作せよ、という問題です。以下のように、元の型に含まれるプロパティのうち指定したものだけ残すような型 MyPick
を作ります。
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
この問題も、Mapped Types を使用します。
type MyPick<T, K> = { [Key in XXX]: YYY }
XXX
を第2型引数の K
にすれば、プロパティが K
のオブジェクトになりそうです。また、YYY
は先ほどと同様に、T[Key]
とすればよさそうです。
type MyPick<T, K> = { [Key in K]: T[Key] }
ですが、Playground で試してみると、エラーが残っています。
テストケースのエラーに注目します。3つ目のテストケースはエラーになることを想定していますが、エラーになっていないようです。このテストケースでは、 Todo
のプロパティには存在しない invalid
があり、Pick
はこの場合にエラーになります。
これを防ぐためには、extends
を使用した 型引数の制約 を行う必要があります。K
が keyof T
の 部分型1 になっていればよいので K extends keyof T
と書きます。型引数の制約をすると、以下のようになります。
type MyPick<T, K extends keyof T> = { [Key in K]: T[Key] }
これですべてのエラーがなくなりました!!
おわりに
たった2問解いただけで、ユーティリティ型・Mapped Types・keyof・Lookup Types・型引数の制約など、様々な TypeScriptの知識が得られます。このように、問題形式で楽しみながら TypeScript の型の勉強ができるので、type-challenges はおすすめです!
僕はまだ数問しか解いていませんが、今後もっと問題を解いていき、余裕があったら解説も記事にしていこうと思います。
おまけ
本記事で出てきた TypeScript の型に関する知識はそれぞれ以下でさらに深めることができます。
- ユーティリティ型
- Mapped Types
- keyof・Lookup Types (Indexed Access Types)
- 型引数の制約
-
部分型に関して、以前に関連する記事を書いているので、ぜひ読んでみてください!
TypeScript の型のエラーはどんなときに発生するのか?~部分型と変性で学ぶ TypeScript の型システム~ ↩