Type Challenges を解く（2）【中級編】

## はじめに

以下の記事の続きです。

https://qiita.com/SiZK/items/15db6e84fcdb654c076d

中級編は100問近くあるので、5つに分割して記事にしていきます。
今回は中級編の最初の記事です。

## 中級編

### 2・Get Return Type

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00002-medium-return-type/README.ja.md)

初級編の [3312・Parameters](https://github.com/type-challenges/type-challenges/blob/main/questions/03312-easy-parameters/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#3312parameters)) でやったものとほとんど同じです。
`infer` を戻り値の型に対して使えば良いです。

```ts
type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer U
    ? U
    : never
```


### 3・Omit

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00003-medium-omit/README.ja.md)

初級編の [43・Exclude](https://github.com/type-challenges/type-challenges/blob/main/questions/00043-easy-exclude/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#43exclude)) と [4・Pick](https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#4pick)) の組み合わせで実現できます。
これらは TypeScript がデフォルトで提供しているユーティリティ型なので、それを使えば良いです。

```ts
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
```

### 8・Readonly 2

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00008-medium-readonly-2/README.ja.md)

`K` が指定されていないとき、という条件を無視して考えると [7・Readonly](https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#7readonly)) ・ [4・Pick](https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#4pick)) と先ほどの [3・Omit](#3omit) の組み合わせ(Intersection Types)で解けます。

```ts
type MyReadonly2<T, K extends keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
```

全てを `readonly` にするというのは、型 `K` に全てのプロパティが指定されている、と言い換えることができます。
型引数にもデフォルト引数を利用できるので、デフォルトの型を `keyof T` とすることで解くことができます。

```ts
type MyReadonly2<T, K extends keyof T = keyof T> =
  Readonly<Pick<T, K>> & Omit<T, K>
```

### 9・Deep Readonly

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00009-medium-deep-readonly/README.ja.md)

[7・Readonly](https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#7readonly)) と似た処理をして、プロパティの値が `object` であるかどうかを判断して再帰をすれば良さそうです。

```ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K]
}
```

問題文には、

> 配列、関数、クラスなどは考慮する必要はありません。

と書いてありますが、テストケースには追加されているので、プロパティの値が関数となっているところで、うまくいっていないようです。
したがって、関数かどうかの分岐も追加します。

```ts
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
}
```

### 10・Tuple to Union

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00010-medium-tuple-to-union/README.ja.md)

[11・Tuple to Object](https://github.com/type-challenges/type-challenges/blob/main/questions/00011-easy-tuple-to-object/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#11tuple-to-object)) でやったような Indexed Access Types の使い方をすれば良いです。

```ts
type TupleToUnion<T extends any[]> = T[number]
```

### 12・Chainable Options

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00012-medium-chainable-options/README.ja.md)

関数 `option` は新しい `Chainable` 型を返し、関数 `get` は `option` の引数から導かれる型を返すようにしたいです。
これはデフォルト型引数を用いて、うまく再帰をすることで実現できます。

```ts
type Chainable<T = {}> = {
  option<K extends string, V>(key: K, value: V):
    Chainable<T & { [_ in K]: V }>
  get(): T
}
```

テストケースを見ると、関数 `option` で同じキーが使われたときは、プロパティの値の型を上書きしなければならないようです。
これは [3・Omit](#3omit) を用いれば良いです。

```ts
type Chainable<T = {}> = {
  option<K extends string, V>(key: K, value: V):
    Chainable<Omit<T, K> & { [_ in K]: V }>
  get(): T
}
```

まだ波線が出ています。
キーが重複しているときには型エラーとなるようにしたいようです。
とはいえ `option` の戻り値の型は現状から変えるとテストが通らなくなってしまいます。
そこで、引数 `key` が重複しているときは、引数の型を適当な型にすることで、型エラーが起こるようにします。

```ts
type Chainable<T = {}> = {
  option<K extends string, V>(
    key: K extends keyof T ? never : K,
    value: V
  ): Chainable<Omit<T, K> & { [_ in K]: V }>
  get(): T
}
```

### 15・Last of Array

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00015-medium-last/README.ja.md)

Variadic Tuple Types と `infer` によって実現できます。

```ts
type Last<T extends any[]> = T extends [...any, infer U] ? U : never
```

### 16・Pop

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00016-medium-pop/README.ja.md)

[15・Last of Array](#15last-of-array) と殆ど同じです。

```ts
type Pop<T extends any[]> = T extends [...infer U, any] ? U : T
```

### 20・Promise.all

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00020-medium-promise-all/README.ja.md)

[189・Awaited](https://github.com/type-challenges/type-challenges/blob/main/questions/00189-easy-awaited/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#189awaited)) を使って配列を処理すれば良さそうです。

```ts
declare function PromiseAll<T extends any[]>(values: T):
  Promise<{[K in keyof T]: Awaited<T[K]>}>
```

テストケースを見ると `as const` としていないものでも、特段の指定がない限りはタプルとして扱う必要があるようです。
なので Variadic Tuple Types で基本的にタプルとみなすようにします。

```ts
declare function PromiseAll<T extends any[]>(values: [...T]):
  Promise<{[K in keyof T]: Awaited<T[K]>}>
```

### 62・Type Lookup

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00062-medium-type-lookup/README.ja.md)

Distributive Conditional Types によって実現できます。

```ts
type LookUp<U, T> = U extends { type: T } ? U : never
```

### 106・Trim Left

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00106-medium-trimleft/README.ja.md)

`string` の処理は Template Literal Types を用いると良いです。
JavaScript のテンプレート文字列と同じようなことを型でも行うことができます。

```ts
type TrimLeft<S extends string> = S extends `${' '}${infer Rest}` ? TrimLeft<Rest> : S
```

テストケースを見ると、改行やタブ文字も取り除く必要があるようなので Union Types を使います。

```ts
type Whitespace = ' ' | '\n' | '\t'
type TrimLeft<S extends string> =
  S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S
```

### 108・Trim

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00108-medium-trim/README.ja.md)

[106・Trim Left](#106trim-left) を右からも行うようにすればよいです。

```ts
type Whitespace = ' ' | '\n' | '\t'
type TrimLeft<S extends string> =
  S extends `${Whitespace}${infer Rest}` ? TrimLeft<Rest> : S
type TrimRight<S extends string> =
  S extends `${infer Rest}${Whitespace}` ? TrimRight<Rest> : S
type Trim<S extends string> = TrimLeft<TrimRight<S>>
```

### 110・Capitalize

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00110-medium-capitalize/README.ja.md)

気合いで解決します。
`a: 'A'` みたいなマッピングを全部書くのは大変なので、適当なプログラムを書いて出力したものを貼り付けると良いです。

```ts
type LowerToUpper = {
  a: 'A'
  b: 'B'
  c: 'C'
  d: 'D'
  e: 'E'
  f: 'F'
  g: 'G'
  h: 'H'
  i: 'I'
  j: 'J'
  k: 'K'
  l: 'L'
  m: 'M'
  n: 'N'
  o: 'O'
  p: 'P'
  q: 'Q'
  r: 'R'
  s: 'S'
  t: 'T'
  u: 'U'
  v: 'V'
  w: 'W'
  x: 'X'
  y: 'Y'
  z: 'Z'
}
type MyCapitalize<S extends string> =
  S extends `${infer First extends keyof LowerToUpper}${infer Rest}`
    ? `${LowerToUpper[First]}${Rest}`
    : S
```

### 116・Replace

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00116-medium-replace/README.ja.md)

一度だけ、とあるので再帰を使わずに素直に書けば良さそうです。

```ts
type Replace<S extends string, From extends string, To extends string> =
  S extends `${infer L}${From}${infer R}`
    ? `${L}${To}${R}`
    : S
```

テストケースを見ると `From` が `''` となっているケースがあるので、このときは置き換えないようにします。

```ts
type Replace<S extends string, From extends string, To extends string> =
  From extends ''
    ? S
    : S extends `${infer L}${From}${infer R}`
      ? `${L}${To}${R}`
      : S
```

### 119・ReplaceAll

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00119-medium-replaceall/README.ja.md)

[116・Replace](#116replace) を再帰で書けばよいです。

```ts
type ReplaceAll<S extends string, From extends string, To extends string> =
  From extends ''
    ? S
    : S extends `${infer L}${From}${infer R}`
      ? `${L}${To}${ReplaceAll<R, From, To>}`
      : S
```

### 191・Append Argument

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00191-medium-append-argument/README.ja.md)

関数の処理は [3312・Parameters](https://github.com/type-challenges/type-challenges/blob/main/questions/03312-easy-parameters/README.ja.md) ([解説](https://qiita.com/SiZK/items/15db6e84fcdb654c076d#3312parameters)) や [2・Get Return Type](#2get-return-type) と同じようなことをすれば良いです。
引数の追加は Variadic Tuple Types をうまく使うことで実現できます。

```ts
type AppendArgument<Fn extends (...args: any[]) => any, A> =
  Fn extends (...args: infer Args) => infer T
    ? (...args: [...Args, A]) => T
    : never
```

### 296・Permutation

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00296-medium-permutation/README.ja.md)

Union Types から型を一つだけを取り出すためには Distributive Conditional Types を用います。
その後、引数として与えられた型から Distributive Conditional Types で取り出した型を除いて、再帰すれば良さそうです。

```ts
type Permutation<T, U = T> = T extends any
  ? [T, ...Permutation<Exclude<U, T>>]
  : never
```

引数に何を与えても `never` になってしまっています。
これは `T` が `never` のときに `T extends` が `never` を返すという Distributive Conditional Types の挙動があり、その `never` を `...Permutation<Exclude<U, T>>` で展開しようとしているためです(`...` を外して型を見ると分かりやすいです)。

そのため `T` が `never` のとき、という条件を加えると良さそうです。先程言及した通り `T extends` というように条件を書くと Distributive Conditional Types が働いてしまうので `[]` で囲って抑制します。

```ts
type Permutation<T, U = T> = [T] extends [never]
  ? []
  : T extends any
    ? [T, ...Permutation<Exclude<U, T>>]
    : []
```

### 298・Length of String

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00298-medium-length-of-string/README.ja.md)

Template Literal Types と Variadic Tuple Types を用いることで、文字列型をタプルに直すことができ、そのタプル型の `length` を取得することで実現できます。

```ts
type LengthOfString<S extends string, T extends any[] = []> =
  S extends `${infer First}${infer Rest}`
    ? LengthOfString<Rest, [...T, First]>
    : T['length']
```

### 459・Flatten

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00459-medium-flatten/README.ja.md)

Variadic Tuple Types でタプルの最初の部分を取り出し、それが配列なら展開をして再帰、そうでなければ残りの部分で再帰、とすることで実現できます。
一度 flatten メソッドを実装してみると、型でのやり方も分かりやすくなると思います。

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

### 527・Append to object

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00527-medium-append-to-object/README.ja.md)

Intersection Types を用いれば良さそうです。

```ts
type AppendToObject<T, U extends PropertyKey, V> =
  T & { [K in U]: V }
```

一つもテストが通っていないようです。
テストで使用している `Equal` では Intersection Types を使ったものとそうでないものを区別しているようです。
[8・Readonly 2](#8readonly-2) で Intersection Types を使っているのにも関わらずうまく行っているのは `Readonly` での処理が良い役割を果たしているからのようです。
したがって Mapped Types を追加で用いるように変更します。

```ts
type Recalc<T> = { [K in keyof T]: T[K] }
type AppendToObject<T, U extends PropertyKey, V> =
  Recalc<T & { [K in U]: V }>
```

### 529・Absolute

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00529-medium-absolute/README.ja.md)

Template Literal Types で数値型を文字列型にし、マイナスの記号があるかどうかで場合分けをすることで実現できます。

```ts
type Absolute<T extends number | string | bigint> = 
  `${T}` extends `-${infer N}` ? N : `${T}`
```

### 531・String to Union

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00531-medium-string-to-union/README.ja.md)

[298・Length of String](#298length-of-string) と似た処理をすることで実現できます。

```ts
type StringToUnion<T extends string, U = never> =
  T extends `${infer First}${infer Rest}`
    ? StringToUnion<Rest, U | First>
    : U
```

### 599・Merge

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/00599-medium-merge/README.ja.md)

[3・Omit](#3omit) と Intersection Types を用いれば良さそうです。

```ts
type Merge<F, S> = Omit<F, keyof S> & S
```

Intersection Types を用いると [527・Append to object](#527append-to-object) の時と同じ問題が生じるので、同じように対策します。

```ts
type Recalc<T> = { [K in keyof T]: T[K] }
type Merge<F, S> = Recalc<Omit<F, keyof S> & S>
```

## おわりに

これで、中級編の 1/5 が解き終わりました。
今回は特に文字列型の知識を強化することができたと思います。

残りの問題も解き次第記事にしていこうと思います。