【TypeScript】型の問題集 type-challenges に挑戦！【解説つき】

# はじめに

この記事では、TypeScript の型の問題集 type-challenges についての簡単な説明を行い、例として数問の解き方と関連する TypeScript の機能を紹介します。

対象読者として、プリミティブ型、オブジェクト、ジェネリクス等、TypeScript の型に関する基礎知識が習得済みの方や、もっと TypeScript の型を使いこなせるようになりたい方を対象とします。

# type-challenges とは

TypeScript の型に関する問題集です。以下のリポジトリに問題がまとまっています。

https://github.com/type-challenges/type-challenges

難易度は `warm-up`, `easy`, `medium`, `hard`, `extreme` となっています。難易度 `warm-up` は例題なので、実際は難易度 `easy` から取り組むことになると思いますが、難易度 `easy` でもそこそこ難しいです。

使い方の詳細は以下の記事が参考になります。

https://qiita.com/ryo2132/items/925b96838dd8cca7cebd

上記の記事で紹介された時よりも、問題数がかなり増えています！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325392/0f70d325-f590-6710-6d98-be9a4a0843bb.png)

それでは実際に解いてみましょう！

# type-challenges に挑戦！

この記事では、例として、難易度 `warm-up` を1問と、難易度 `easy` を2問、問題とそれを解くために必要な TypeScript の知識・機能を解説します。

## 【難易度: warm-up】13・Hello World

問題は以下のページから確認できます。

https://github.com/type-challenges/type-challenges/tree/main/questions/00013-warm-hello-world

この問題は、 `HelloWorld` という名前で、`string` と同じになる型を作る問題です。

```typescript
// expected to be string
type HelloWorld = any
```

また、以下の test 型で、回答が正しいか確認されます。

```typescript
// you should make this work
type test = Expect<Equal<HelloWorld, string>>
```

`Take the Challenge` ボタンから、Playground を立ち上げると、テスト用の型がエラーになっていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325392/52f359aa-7991-d2a9-293a-bc82af3f9c60.png)

`any` を `string` に変えると、エラーがなくなります。

```typescript
// expected to be string
type HelloWorld = string
```

このように、type-challenges は特定の条件を満たすような型を作成し、テスト用の型のエラーをなくせば正解です！

この問題は例題なので、さすがに簡単ですね。難易度 `easy` の問題を解いてみましょう！

## 【難易度: easy】7・Readonly

問題は以下のページから確認できます。

https://github.com/type-challenges/type-challenges/tree/main/questions/00007-easy-readonly

ユーティリティ型の `Readonly` を自作する、という問題です。
TypeScript には 様々なユーティリティ型が存在し、それを使うことで組み込みのジェネリクス型の変換がしやすくなります。

`Readonly` は、オブジェクト型の各プロパティを `readonly` にする、というユーティリティ型です。
例として、`Readonly<{ x: number }>` は `{ readonly x: number }` になります。

この型を作るために必要な型の知識として、**Mapped Types** を紹介します。
Mapped Types は以下のような構文の型です。 `K` は型変数、`T` `S` には何か具体的な型を当てはめます。

```typescript
{ [K in T]: S }
```

例として、`T` に `'x' | 'y'`、`S` に `number` を当てはめてみます。

```typescript
{ [K in 'x' | 'y']: number }
```

この型は、`{ x: number,　y: number }` と同じ型になります。
これだけだとインデックス型に似ていますが、インデックス型とは異なり、プロパティのキーを制限できます。
また、それだけでなく、オブジェクトの各プロパティに対し、型の変換ができます。具体的には、`readonly` や `?` の付与・除去など、他にもいろいろなことができます。したがって、今回の問題は Mapped Types を使って `readonly` を付与すればよさそうですね。

問題形式を見てみましょう。以下のようになっています。

```typescript
type MyReadonly<T> = any
```

`any` を一旦 `readonly` を付与する Mapped Types に置き換えてみます。
以下のように、`+readonly` を追加することで、各プロパティは `readonly` が付いた型になります。

```typescript
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` を自作することができました！

```typescript
type MyReadonly<T> = { +readonly [K in keyof T]: T[K] }
```

もう１問解いてみましょう！

## 【難易度: easy】4・Pick

問題は以下のページから確認できます。

https://github.com/type-challenges/type-challenges/tree/main/questions/00004-easy-pick

組み込みのユーティリティ型 `Pick` を自作せよ、という問題です。以下のように、元の型に含まれるプロパティのうち指定したものだけ残すような型 `MyPick` を作ります。

```typescript
interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}
```

この問題も、Mapped Types を使用します。

```typescript
type MyPick<T, K> = { [Key in XXX]: YYY }
```

`XXX` を第2型引数の `K` にすれば、プロパティが `K` のオブジェクトになりそうです。また、`YYY` は先ほどと同様に、`T[Key]` とすればよさそうです。

```typescript
type MyPick<T, K> = { [Key in K]: T[Key] }
```

ですが、Playground で試してみると、エラーが残っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325392/533f5c90-8300-26d2-fe9d-3eb67c5c999d.png)

テストケースのエラーに注目します。3つ目のテストケースはエラーになることを想定していますが、エラーになっていないようです。このテストケースでは、 `Todo` のプロパティには存在しない `invalid` があり、`Pick` はこの場合にエラーになります。

これを防ぐためには、`extends` を使用した **型引数の制約** を行う必要があります。`K` が `keyof T` の 部分型[^1] になっていればよいので `K extends keyof T` と書きます。型引数の制約をすると、以下のようになります。

```typescript
type MyPick<T, K extends keyof T> = { [Key in K]: T[Key] }
```

これですべてのエラーがなくなりました！！

# おわりに

たった2問解いただけで、ユーティリティ型・Mapped Types・keyof・Lookup Types・型引数の制約など、様々な TypeScriptの知識が得られます。このように、問題形式で楽しみながら TypeScript の型の勉強ができるので、type-challenges はおすすめです！

僕はまだ数問しか解いていませんが、今後もっと問題を解いていき、余裕があったら解説も記事にしていこうと思います。

# おまけ

本記事で出てきた TypeScript の型に関する知識はそれぞれ以下でさらに深めることができます。

- ユーティリティ型
    - https://www.typescriptlang.org/docs/handbook/utility-types.html
    - [ユーティリティ型 (utility type) | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/type-reuse/utility-types)
- Mapped Types
    - https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
    - [Mapped Types | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/type-reuse/mapped-types)
- keyof・Lookup Types (Indexed Access Types)
    - https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types
    - https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html
    - [keyof型演算子 | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/type-reuse/keyof-type-operator)
    - [インデックスアクセス型 (indexed access types) | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/type-reuse/indexed-access-types)
- 型引数の制約
    - https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints
    - [型引数の制約 | TypeScript入門『サバイバルTypeScript』](https://typescriptbook.jp/reference/generics/type-parameter-constraint)

[^1]:部分型に関して、以前に関連する記事を書いているので、ぜひ読んでみてください！
[TypeScript の型のエラーはどんなときに発生するのか？～部分型と変性で学ぶ TypeScript の型システム～](https://qiita.com/wataru86/items/9956feb5b0f42fd49dc9)