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

## はじめに

以下の記事の続きです。

- [Type Challenges を解く【初級編】](https://qiita.com/SiZK/items/15db6e84fcdb654c076d)
- [Type Challenges を解く（2）【中級編】](https://qiita.com/SiZK/items/6b65e088a2cede2320dd)

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

## 中級編

### 612・KebabCase

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

[110・Capitalize](https://github.com/type-challenges/type-challenges/blob/main/questions/00110-medium-capitalize/README.ja.md) ([解説](https://qiita.com/SiZK/items/6b65e088a2cede2320dd#110capitalize)) のような小文字・大文字周りの型を頑張って作っていけば、うまくできると思いますが、少々面倒なので [Intrinsic String Manipulation Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#intrinsic-string-manipulation-types) を使っていきます。

基本的には大文字が出てきたら、その前にハイフンを付けるという処理をすれば良さそうです。
ただし、一文字目だけはハイフンを付けないようにする必要があります。
また、全ての文字は最終的に小文字にする必要があります。

```ts
type KebabCase<S extends string> =
  S extends `${infer First}${infer Rest}`
    ? Rest extends Uncapitalize<Rest>
      ? `${Lowercase<First>}${KebabCase<Rest>}`
      : `${Lowercase<First>}-${KebabCase<Rest>}`
    : S
```

最初の条件分岐で一文字目とそれ以降に分けることで、うまく処理をしています。
また `Uncapitalize` は先頭の文字を小文字にする型です。

### 645・Diff

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

互いに存在しているプロパティを取り除いた型が取得できれば、その型の Intersection Types を取れば良さそうです。

```ts
type Diff<O, O1> = Omit<O, keyof O1> & Omit<O1, keyof O>
```

Type Challenges において Intersection Types を用いると `Equal` による問題が生じるので、[前回](https://qiita.com/SiZK/items/6b65e088a2cede2320dd#527append-to-object)も行った対策をします。

```ts
type Recalc<T> = { [K in keyof T]: T[K] }
type Diff<O, O1> = Recalc<Omit<O, keyof O1> & Omit<O1, keyof O>>
```

### 949・AnyOf

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

Falsy な値を列挙して、そうでないものが含まれているならば `true` を返すようにします。

```ts
type Falsy =
  | 0
  | ''
  | false
  | []
  | { [Key: string]: never }
  | undefined
  | null
type AnyOf<T extends readonly any[]> = T extends Falsy[] ? false : true
```

`{ [Key: string]: never }` の部分を単に `{}` とすると、任意のオブジェクトが部分型と判定されてしまうので、このように回避しています。

### 1042・IsNever

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

`T extends never` で良いように思えますが `T` に `never` が入ると Distributive Conditional Types の挙動で `never` が返されてしまいます。
そのため Distributive Conditional Types の働きを抑制するようにします。

```ts
type IsNever<T> = [T] extends [never] ? true : false
```

### 1097・IsUnion

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

Distributive Conditional Types を使って分配されているなら Union Types であると判定できます。

```ts
type IsUnion<T, U = T> = T extends T
  ? [U] extends [T]
    ? false
    : true
  : never
```

テストケースを見ると `T` に `never` が入っているときにうまくいっていないようです。
これは先ほど言及したように Distributive Conditional Types の挙動なので `T` が `never` のとき、という条件分岐を加えます。

```ts
type IsUnion<T, U = T> = [T] extends [never]
  ? false
  : T extends T
    ? [U] extends [T]
      ? false
      : true
    : never
```

### 1130・ReplaceKeys

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

Union Types から型を一つだけを取り出すために Distributive Conditional Types を用います。
あとは Mapped Types で指示通りやれば実現できます。

```ts
type ReplaceKeys<U, T, Y> = U extends any
  ? {
    [K in keyof U]: K extends T
      ? K extends keyof Y
        ? Y[K]
        : never
      : U[K]
  }
  : never
```

実は下記のように、最初に Conditional Types を使わなくても、何故か Union Types が分配されてうまくいきますが、いまいち理由が分かっていません。
分かる方がいれば教えていただけると助かります。

```ts
type ReplaceKeys<U, T, Y> = {
  [K in keyof U]: K extends T
    ? K extends keyof Y
      ? Y[K]
      : never
    : U[K]
}
```

### 1367・Remove Index Signature

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/01367-medium-remove-index-signature/README.md)

Mapped Types で Key Remapping を使って Index Signature を取り除けば良いです。
一般的なプロパティ名の型は `string | number | symbol` の部分型であるのに対し、Index Signature は `string | number | symbol` のいずれかと一致します。
また `string | number | symbol` は `PropertyKey` として定義されているので、これを利用します。

```ts
type RemoveIndexSignature<T> = {
  [K in keyof T
    as [PropertyKey] extends [Exclude<PropertyKey, K>]
      ? K
      : never
  ]: T[K]
}
```

`PropertyKey` からプロパティ名の型を除外したものが `PropertyKey` と一致している場合は、除外が発生しておらず、一般的なプロパティ名になっていると判断できます。
そうでない場合は Index Signature を使っているために、プロパティ名の型が `PropertyKey` のいずれかに含まれており、除外が発生していると判断できます。

Distributive Conditional Types の抑制のために `[]` をつけていますが、つけなくてもうまくいくみたいです。
おそらく Key Remapping のときには、プロパティ名が Union Types とならないように、分配しないようになっているのだと思います。

### 1978・Percentage Parser

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/01978-medium-percentage-parser/README.md)

符号がある場合とない場合、パーセントの記号がある場合とない場合、の4通りの組み合わせを走査すればよいです。

```ts
type Sign = '+' | '-'
type PercentageParserWithoutSign<A extends string> =
  A extends `${infer N}%` ? [N, '%'] : [A, '']
type PercentageParser<A extends string> =
  A extends `${infer First extends Sign}${infer Rest}`
    ? [First, ...PercentageParserWithoutSign<Rest>]
    : ['', ...PercentageParserWithoutSign<A>]
```

### 2070・Drop Char

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/02070-medium-drop-char/README.md)

[119・ReplaceAll](https://github.com/type-challenges/type-challenges/blob/main/questions/00119-medium-replaceall/README.ja.md) ([解説](https://qiita.com/SiZK/items/6b65e088a2cede2320dd#119replaceall)) の `To` が `''` になったものです。

```ts
type DropChar<S extends string, C extends string> =
  S extends `${infer L}${C}${infer R}`
    ? `${L}${DropChar<R, C>}`
    : S
```

### 2257・MinusOne

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

繰り下がりの有無で場合分けをします。
このとき、数値型のままだと場合分けが難しいので、文字列型にしてから処理をしていき、最後に数値型へ戻します。
また、素直に場合分けをしようと思うと、一番右の文字を取り出したくなりますが、使われる文字種が同じ場合に右の文字を取り出すのは難しいです。
そのため、文字列を反転してから処理をし、最後にまた反転して元に戻します。

```ts
type Reverse<S extends string> =
  S extends `${infer First}${infer Rest}`
    ? `${Reverse<Rest>}${First}`
    : S
type ParseInt<S extends string> =
  S extends `${infer N extends number}` ? N : never

type MinusOneChar = {
  0: '9'
  1: '0'
  2: '1'
  3: '2'
  4: '3'
  5: '4'
  6: '5'
  7: '6'
  8: '7'
  9: '8'
}
type MinusOneString<T extends string> =
  T extends `${infer N extends keyof MinusOneChar}${infer M}`
    ? `${MinusOneChar[N]}${N extends 0 ? MinusOneString<M> : M}`
    : never
type MinusOne<T extends number> =
  ParseInt<Reverse<MinusOneString<Reverse<`${T}`>>>>
```

`100` のときにうまくいっていないようです。
これは、先頭に `'0'` があるせいで `ParseInt` が `number` を返すためです。(ref: [Improved Inference for `infer` Types in Template String Types](https://devblogs.microsoft.com/typescript/announcing-typescript-4-8-beta/#improved-inference-for-infer-types-in-template-string-types))
したがって、先頭の `'0'` を取り除くようにします。

```ts
type Reverse<S extends string> =
  S extends `${infer First}${infer Rest}`
    ? `${Reverse<Rest>}${First}`
    : S
type RemoveLeadingZeros<S extends string> =
  S extends '0'
    ? S
    : S extends `0${infer N}`
      ? RemoveLeadingZeros<N>
      : S
type ParseInt<S extends string> =
  RemoveLeadingZeros<S> extends `${infer N extends number}`
    ? N
    : never

type MinusOneChar = {
  0: '9'
  1: '0'
  2: '1'
  3: '2'
  4: '3'
  5: '4'
  6: '5'
  7: '6'
  8: '7'
  9: '8'
}
type MinusOneString<T extends string> =
  T extends `${infer N extends keyof MinusOneChar}${infer M}`
    ? `${MinusOneChar[N]}${N extends 0 ? MinusOneString<M> : M}`
    : never
type MinusOne<T extends number> =
  ParseInt<Reverse<MinusOneString<Reverse<`${T}`>>>>
```

> Given a number (always positive)

と書いてあるのにも関わらず `0` があるので、うまくいっていないようです。
負数などが与えられた場合も考慮して変更してもよいのですが、殆ど同じようなコードを2回書く必要があり、かなり長くなってしまうので、ここでは `0` に対する処理のみを追加します(positive を自然数と解釈するのは気持ちが悪いので、 `0` を `+0` と解釈することで納得しておきます)。

```ts
type MinusOne<T extends number> =
  T extends 0
    ? -1
    : ParseInt<Reverse<MinusOneString<Reverse<`${T}`>>>>
```

毎回載せるのは長いので、今回は変更した部分のみコードを載せました。

### 2595・PickByType

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

Mapped Types で Key Remapping を使えば良いです。

```ts
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
}
```

### 2688・StartsWith

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

先頭が `U` である文字列型かどうかを判定すれば良いです。

```ts
type StartsWith<T extends string, U extends string> =
  T extends `${U}${string}` ? true : false
```

### 2693・EndsWith

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

先ほどと同じようなことをすれば良いです。

```ts
type EndsWith<T extends string, U extends string> =
  T extends `${string}${U}` ? true : false
```

### 2757・PartialByKeys

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

`K` を取り除いたものと `K` のみのプロパティをオプションプロパティにしたものの Intersection Types を取れば良さそうです。
また Intersection Types での `Equal` の問題が生じるので、その対策もしておきます。

```ts
type Recalc<T> = { [K in keyof T]: T[K] }
type PartialByKeys<T, K extends keyof T> =
  Recalc<Omit<T, K> & Partial<Pick<T, K>>>
```

第二引数がない場合にも対応する必要があるようです。
この場合は全てのプロパティ名が指定されているのと同じにすれば良いようです。

```ts
type Recalc<T> = { [K in keyof T]: T[K] }
type PartialByKeys<T, K extends keyof T = keyof T> =
  Recalc<Omit<T, K> & Partial<Pick<T, K>>>
```

### 2759・RequiredByKeys

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

先程と同じようなことをすれば良いです。

```ts
type Recalc<T> = { [K in keyof T]: T[K] }
type RequiredByKeys<T, K extends keyof T = keyof T> =
  Recalc<Omit<T, K> & Required<Pick<T, K>>>
```

### 2793・Mutable

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

Mapped Types で `readonly` を外す修飾子を指定すれば良いです。(ref: [Mapping Modifiers](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers))

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

### 2852・OmitByType

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

[2595・PickByType](#2595pickbytype) と逆にするだけです。

```ts
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K]
}
```

### 2946・ObjectEntries

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

Distributive Conditional Types を用いて、プロパティ名を一つずつ取り出せば、目的の値を作ることができそうです。

```ts
type ObjectEntries<T, K extends keyof T = keyof T> =
  K extends any ? [K, T[K]] : never
```

オプションプロパティのときは `undefined` を取り除いたものを返す必要があるようです。
これは `Required` したプロパティを使うようにすれば良さそうです。

```ts
type ObjectEntries<T, K extends keyof T = keyof T> =
  K extends any ? [K, Required<T>[K]] : never
```

オプションプロパティの型が `undefined` のみのときに `never` を返してしまっているようです。
`never` のときは `undefined` を返すように変更します。

```ts
type ObjectEntries<T, K extends keyof T = keyof T> =
  K extends any
    ? [K, Required<T>[K] extends never ? undefined : Required<T>[K]]
    : never
```

### 3062・Shift

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

[15・Last of Array](https://github.com/type-challenges/type-challenges/blob/main/questions/00015-medium-last/README.ja.md) ([解説](https://qiita.com/SiZK/items/6b65e088a2cede2320dd#15last-of-array)) と似たようなことをします。

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

### 3188・Tuple to Nested Object

[問題](https://github.com/type-challenges/type-challenges/blob/main/questions/03188-medium-tuple-to-nested-object/README.md)

Variadic Tuple Types と `infer` を用いて、最初の要素を取り出し、再帰をすれば良いです。

```ts
type TupleToNestedObject<T, U> =
  T extends [infer First extends PropertyKey, ...infer Rest]
    ? { [_ in First]: TupleToNestedObject<Rest, U> }
    : U
```

### 3192・Reverse

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

最初の要素を取り出して後ろに置く、という処理の再帰をすれば良いです。

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

## おわりに

中級編の 2/5 が解き終わりました。
かなり似た問題が多くなってきた印象があります。
もしかしたら順番を変えて、上級編を先に解いて記事にするかもしれないです。