【TypeScript】type-challenges の 難易度 easy 全問解いてみた！【解説つき】

# はじめに

先日、[type-challenges](https://github.com/type-challenges/type-challenges) を紹介する以下の記事を投稿しました。

https://qiita.com/wataru86/items/f1356b5d1aef30012a20

上記の記事では難易度 `warm-up` 1問（13・Hello World）と、難易度 `easy` の2問（7・Readonly、4・Pick）を解説しました。

そして、この記事では難易度 `easy` の全13問(2023/03/15時点)のうち、上記を除く残りの11問をすべて解説します！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325392/76c854bf-6f2f-5573-51c5-d86dea889e4f.png)

# 難易度 `easy` 残りの11問をすべて解説します！

それぞれの問題で必要な知識の都合上、以下の順番で説明します。

- 18・Length of Tuple
- 11・Tuple to Object
- 3057・Push
- 3060・Unshift
- 268・If
- 14・First of Array
- 43・Exclude
- 189・Awaited
- 533・Concat
- 898・Includes
- 3312・Parameters

## 18・Length of Tuple

https://github.com/type-challenges/type-challenges/tree/main/questions/00018-easy-tuple-length

タプルの型が与えられるので、その長さの[リテラル型](https://typescriptbook.jp/reference/values-types-variables/literal-types)を返す型 `Length` を作成する問題です。

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

[タプル型](https://typescriptbook.jp/reference/values-types-variables/tuple)は、`[string, number, boolean]` のような各要素の型や要素の数が決まっている配列の型です。配列では `arr.length` のように `length` で長さが取得できました。タプルも同様に `length` で長さを求めることができます。タプルは要素数が決まっているので、`length` の型が判明すればよさそうです。

前回の記事でも少し紹介しましたが、[インデックスアクセス型](https://typescriptbook.jp/reference/type-reuse/indexed-access-types)でオブジェクトの型からそのプロパティの型を求めることができます。今回は `T['length']` のように書くことで、タプル型から `length` の型が取得できます。

```typescript
type Length<T> = T['length']
```

そして、「T はタプル型である (= `length` プロパティを持つ) 」という[型引数の制約](https://typescriptbook.jp/reference/generics/type-parameter-constraint)をつけたら完成です！

```typescript
type Length<T extends readonly any[]> = T['length']
```


## 11・Tuple to Object

https://github.com/type-challenges/type-challenges/tree/main/questions/00011-easy-tuple-to-object

タプル型が与えられ、各要素の key と value を持つオブジェクト型を作成する `TupleToObject` 型を作る問題です。

```typescript
type TupleToObject<T extends readonly any[]> = any
```

例として、`['a', 'b']` というタプル型が与えられたとき、`TupleToObject<['a', 'b']>` は `{ 'a': 'a', 'b': 'b'}` 型になれば OK です。[インデックス型](https://typescriptbook.jp/reference/type-reuse/indexed-access-types)を使って、key と value が同じようなプロパティを持つオブジェクト型を作成すればよさそうです。

```typescript
type TupleToObject<T extends readonly any[]> = { [K in ????]: K }
```

では、タプル型から各要素の型を取得するにはどうすればよいでしょうか？配列の場合、 `arr[0]` のように要素の位置を指定すればその要素を取得できました。ここで要素の位置は `0` や `1` のような `number` です。前の問題ではオブジェクト型に `T['length']` のようにプロパティのキーの型を指定することで、その要素の型が取得できました。これと同様に、`T[number]` とすると、タプルの要素の型が取得できます。例として、`T` が `['a', 'b']` の場合、 `T[number]` は `'a' | 'b'` になります。

これでタプルから要素の型を取得できそうなので、それを使って以下のようにインデックス型で指定された型が作れそうです。

```typescript
type TupleToObject<T extends readonly any[]> = { [K in T[number]]: K }
```

ですが、これだけではまだ足りません。以下のように要素が配列やオブジェクトの場合にエラーになってほしいところですが、エラーになっていません。

```typescript
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
```

オブジェクトのキーは `string | number | symbol` のどれかのみ可能です。そこで、その制約をつければ完成です！

```typescript
type TupleToObject<T extends readonly (string | number | symbol)[]> = { [K in T[number]]: K }
```


## 3057・Push

https://github.com/type-challenges/type-challenges/tree/main/questions/03057-easy-push

タプル型 `T` ともう一つの型 `U` が与えられるので、 `Array.push` のように `T` の末尾に `U` を追加した型を作成する問題です。 

```typescript
type Push<T, U> = any
```

型の `push` を考える前に、値の `push` を考えてみましょう。JavaScript および TypeScript では、[スプレッド構文](https://typescriptbook.jp/reference/values-types-variables/array/spread-syntax-for-array) を使うことで配列の展開ができました。例として `arr = [1,2]` のとき、 `[...arr, 3]` と書くことで `[1,2,3]` を作ることができました。実は型でも同様に、`[...T, U]` と書くことができます！

あとは `T` に制限を付けたら完成です！！

```typescript
type Push<T extends any[], U> = [...T, U]
```

## 3060・Unshift

https://github.com/type-challenges/type-challenges/tree/main/questions/03060-easy-unshift

次は `Array.unshift` の型バージョンです。

```typescript
type Unshift<T, U> = any
```

`Array.push` とは逆に、先頭に `U` を追加する問題ですね。これも先程と同様に、型のスプレッド構文で実装できます。制限を付けたら完成です。

```typescript
type Unshift<T extends any[], U> = [U, ...T]
```

## 268・If

https://github.com/type-challenges/type-challenges/tree/main/questions/00268-easy-if

型引数 `C` が `true` 型だったら `T` を、 `C` が `false` 型だったら `F` を返す `IF` 型を作る問題です。

```typescript
type If<C, T, F> = any
```

ついにきました！型の条件分岐です！！TypeScript では、三項演算子を使うことで型の条件分岐ができます。この機能は [Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)と呼ばれています。

Conditional Types は `T extends U ? X : Y` のように記述します。 `T` が `U` の部分型[^1]である場合、 `X` を返し、そうでないとき `Y` を返します。今回の問題では、`C` が `true` の部分型かどうかを条件とすればよさそうです。

[^1]: TypeScript の部分型については [以前書いたこちらの記事](https://qiita.com/wataru86/items/9956feb5b0f42fd49dc9) で紹介しているので、ぜひ読んでみてください！

```typescript
type If<C, T, F> = C extends true ? T : F
```

あとは、`C` が `boolean` となるように制限を追加すれば完成です。

```typescript
type If<C extends boolean, T, F> = C extends true ? T : F
```

## 14・First of Array

https://github.com/type-challenges/type-challenges/tree/main/questions/00014-easy-first

タプル型 `T` を受け取り、その最初の要素の型を返す `First` を作る問題です。

```typescript
type First<T extends any[]> = any
```

配列の最初の要素なので `T[0]` としたら取得できそうです。


```typescript
type First<T extends any[]> = T[0]
```

ですが、これだけでは足りません。テストケースには以下のように `T` が `[]` のときに `never` になってほしい様子です。

```typescript
Expect<Equal<First<[]>, never>>
```

`T` が空の場合は `never`、そうでない場合は `T[0]` を返せばよさそうです。前の問題と同様に、Conditional Typesで解決できます。空の配列の型は `[]` なので、条件は `T extends []` にすればよさそうです。これで完成です！

```typescript
type First<T extends any[]> = T extends [] ? never : T[0]
```

## 43・Exclude

https://github.com/type-challenges/type-challenges/tree/main/questions/00043-easy-exclude

第一型引数から、第二引数の型を除いた型を作る問題です。
例として、`MyExclude<'a' | 'b' | 'c', 'a'>` は `'b' | 'c'` になればOKです。

```typescript
type MyExclude<T, U> = any
```

この問題を解くためには、Conditional Types の Union distribution という性質を知っておく必要があります。

Union distribution とは `T extends X ? Y : Z` の時に、`T` がユニオン型の場合に分配法則のように展開されるという性質です。例として `T = A | B` の時、以下のように展開されます。

```typescript
(A | B) extends X ? Y : Z
 => (A extends X ? Y : Z) | (B extends X ? Y : Z)
```

今回の問題では以下のように、`T` が `'a' | 'b' | 'c'`のとき、それぞれについて `U` の部分型になっている場合に `never` を返し、そうでないときはその型を返す、ということがしたいです。

```typescript
('a' extends U ? never : 'a') | ('b' extends U ? never : 'b') | ('c' extends U ? never : 'c')
```

これはまさに、Union distribution によって分配されるので、以下のように書くだけで完成してしまいます！

```typescript
type MyExclude<T, U> = T extends U ? never : T
```

## 189・Awaited

https://github.com/type-challenges/type-challenges/tree/main/questions/00189-easy-awaited

ユーティリティ型の `Awaited` を作成する問題です。`Awaited` は `Promise<string>` から `string` を取り出す型です。

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

この問題も条件分岐で解けそうです。`T` が `Promise<U>` の場合、`U` を返し、そうでない場合は `never` を返せばよさそうです。しかし、以下のように書くことはできません。なぜなら、`U` は未定義の型変数だからです。

```typescript
type MyAwaited<T> = T extends Promise<U> ? U : never
```

このような場合に、Conditional Types のもう一つの強力な機能 `infer` を使うことで解決できます。 使い方はとっても簡単で、`U` の前に `infer` と書くだけです。こう書くことで、`U` を使うことができます。

```typescript
type MyAwaited<T> = T extends Promise<infer U> ? U : never
```

これで完成かと思いきや、まだ足りません。3つ目のテストケースは以下のようになっています。

```typescript
type Z = Promise<Promise<string | number>>
Expect<Equal<MyAwaited<Z>, string | number>>
```

`Promise` の中にさらに `Promise` がある場合、その中の型を返す必要があります。`U` がまだ `Promise` だった場合、`MyAwaited<U>` を返し、そうでないときは `U` を返すことで、再帰的に、`Promise` の中の型を見つけられそうです。

```typescript
type MyAwaited<T> =
  T extends Promise<infer U>
    ? U extends Promise<any> ?  MyAwaited<U> : U
    : never
```

これで、深くネストされた `Promse` の中の型を取り出すことはできましたが、まだ通らないテストケースがあります。

```typescript
type T = { then: (onfulfilled: (arg: number) => any) => any }
Expect<Equal<MyAwaited<T>, number>>
```

`T` は `Promise` と同等のオブジェクトの型です。このような方も同様に扱うためには `extends Promse` としているところを `extends PromiseLike` に変えることで可能になります。


```typescript
type MyAwaited<T> =
  T extends Promise<infer U>
    ? U extends Promise<any> ?  MyAwaited<U> : U
    : never
```

最後にもう一つ、`T` が `PromseLike` ではない場合にエラーにしたいので、制約を付けたら完成です。

```typescript
// @ts-expect-error
type error = MyAwaited<number>
```

```typescript
type MyAwaited<T extends PromiseLike<any>> =
  T extends PromiseLike<infer U>
    ? U extends PromiseLike<any> ? MyAwaited<U> : U
    : never
```

## 533・Concat

https://github.com/type-challenges/type-challenges/tree/main/questions/00533-easy-concat

`Array.concat` の型バージョンを作成する問題です。

```typescript
type Concat<T, U> = any
```

最初に `T` と `U` が配列になるように、制約をつけておきましょう。

```typescript
type Concat<T extends any[], U extends any[]> = any
```

ここまでの問題で、型による条件分岐と型の再帰が出てきました。型で作る前に、値の `concat` を条件分岐と再帰で作ってみます。

```typescript
function concat(xs: any[], ys: any[]): any[] {
  if (xs.length === 0) {
    return ys
  } else {
    const [first, ...rest] = xs
    return [first, ...concat(rest, ys)]
  }
}
```

あとは、これを型バージョンに書き直すだけです。`T extends [infer First, ...infer Rest]` で分岐することで、`T` が空でない場合と空の場合で分岐できます。空でない場合、`First` と `Rest` を使って再帰部を作り、空の場合に `U` を返したら完成です。

```typescript
type Concat<T extends any[], U extends any[]> =
  T extends [infer First, ...infer Rest]
    ? [First, ...Concat<Rest, U>]
    : U
```

## 898・Includes

https://github.com/type-challenges/type-challenges/tree/main/questions/00898-easy-includes

タプル型 `T` に `U` が含まれているか判定する型を作成する問題です。

```typescript
type Includes<T extends readonly any[], U> = any
```

こちらも先程と同様に、`T extends [infer First, ...infer Rest]` で分岐し、最初の要素が `U` と同じか判定し、異なる場合も再帰的に判定すればよさそうです。

```typescript
type Includes<T extends readonly any[], U> =
  T extends [infer First, ...infer Rest]
    ? Equal<First, U> extends true ? true : Includes<Rest, U>
    : false
```

型が同じか判定する `Equal` は、playground で型をテストする際に使用しているものを利用しました。

```typescript
import type { Equal, Expect } from '@type-challenges/utils'
```

## 3312・Parameters

https://github.com/type-challenges/type-challenges/tree/main/questions/03312-easy-parameters

最後は関数型から引数の型を得る問題です。

```typescript
type MyParameters<T extends (...args: any[]) => any> = any
```

すでに `T` に `(...args: any[]) => any` という制約がついていますが、この中の `args` の型を `infer` で取り出すだけです。取り出せない場合は `never` を返しましょう。

```typescript
type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer Args) => any
    ? Args
    : never
```

# おわりに

いかがでしたか？僕自身まだ理解が浅い部分もあるため、拙い解説になってしまっている部分もあるかもしれませんが、参考になったのなら幸いです。

型レベルプログラミングは制約が多く普段のプログラミングより難しいですが、テストケースを元に少しずつゴールに向かって進んでいくのが楽しかったです。知らないと解けない機能があったりするので、そこはあきらめて解答例を参考にするとよさそうです。また、型の条件分岐や `infer`による推論、再帰ができるので、一旦値バージョンで作成したのち、型に変更すると作りやすいです。答えも一つではなく、別解もあるので、よりエレガントな回答を探す楽しみ方もあると思います！

次は難易度 medium の問題に挑戦しようと思います！また何問か解けたら記事にします！お楽しみに！！