Omit<T>を自分で実装しよう（ワンライナーで）

お正月です。一年の計は元旦にありと言いますし、TypeScriptと仲良くなろうと思って [Type Challenges](https://github.com/type-challenges/type-challenges) を始めました。

# 問題
この中に `Omit<T>` を実装せよ、という難易度mediumの問題があります。

https://github.com/type-challenges/type-challenges/blob/master/questions/3-medium-omit/README.md


```typescript
type MyOmit<T, K> = any; // いい感じに実装する

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

// {title: string} 型になるようにしたい
type TodoPreview = MyOmit<Todo, 'description' | 'title'>
```

# 解答
## 基本
これはもちろんmapped typesを知っていれば一瞬で解ける問題で、ビルトインの `Exlude<T, K>` を使って

```typescript
type MyOmit<T, K> = {
    [P in Exclude<keyof T, K>]: T[P]
}
```

で終わりです。難易度mediumどころではありません。

ところが、これをビルトインの型や自前の型などを使わず**1行で**実現しようとするとちょっと難儀します。単に `Exclude<T, K>` の定義をそのまま展開しようとしてもうまくいきません。

```typescript
type MyOmit_notwork<T, K> = { // ダメ
    [P in (keyof T extends K ? never: keyof T)]: T[P]
}
```

丸カッコ中のconditional typesは `never` か `keyof T` （引数そのもの）かのいずれかを返すので直感的にも明らかですね。

```typescript
type hoge = MyOmit_notwork<Todo, "description"> // Todo型そのものになってしまう
```

[【Playground】](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoHsAm7kG8BQyRyYwYANhAFzIDOYUoA5ocZhLQowA6nog16jEC2LIE6ALbdKkTDQBG6dJTgh8AX3z4wAT24oAsroDyksgH0Q6MAHd0UANYAeVABpkAaQB8yALx4yAD0QciAAwyAhwysRADaAArIoMgAFI4QuugwaMgQAB6QIJi0XsgA-MggEABu0DRpGVmoAJQAujSo8S2a2iFoWOiA0eqAugyAdgyAQQwjgNYMgFYMgMYMgGYMgOoMgH4MgGIMOvooABboTCgBxmaW1nYOLhjYHgBE7Jw8fCBX3gDcGwbIMACuTHD+yIfmMBWGz2JyufrXXJQp6vfC9QDKDCNABYMgFkGQAGDIBFBkA9gyAY2tFoARBjeKG4wAyfwBxxBZ3Bl2QV1IFAgVwAPjcOFxgLxgPwWVcJNJZBBMDD8EA)

提出された解答を眺めていたところ、これを解決しているものがいくつかあり、こんな文法知らんかったわ！　という発見もあったので書きます。怪しい点などありましたら突っ込みをお願いします。なおTypeScript v4.1.3で確認しています。

## その1: asを使う
https://github.com/type-challenges/type-challenges/issues/448

```typescript
type MyOmit<T, K extends keyof T> = {[P in keyof T as P extends K ? never: P] :T[P]}
```

これが一番シンプルでした。キモとなるのはTypeScript v4.1から入った[mapped type 'as' clauses](https://github.com/microsoft/TypeScript/pull/40336)です。

* `as` 句で `P` を別の型にキャストできる
* ということはconditional typesを書くこともできる

というわけでこのような記法が許されます。Template literal typesばかりが注目されていましたがこんな使い方もできるんですね。

[【Playground】](https://www.typescriptlang.org/play?#code/C4TwDgpgBAsiDyBbAlsAPAFQDRQNID4oBeKAbwG0AFKZAOygGsIQB7AMygygEMBnKahAAewCLQAm-XFAD8UWhABuEAE4AuAQF0oajFU0BfAFChIsBCmABBADY3MOAsTJUa9Jqw5c+8pau26+sZGdKIqbNwAxtAAEiwA5tCkRlCpPBq8wCp08QDcKWkARhlZOflpUJEatACuiIWq+cGm0BgAjM5wSKhocYk4AESRAwA+A+IDhAD0U2TpUJnZtHlQxQuly7lQxi2cAEydFqi29n0Qg8NjE-hbM2TGQA)


## その2: 3つめの変数を使う
https://github.com/type-challenges/type-challenges/issues/255

```typescript
type MyOmit<T, K, Keys extends keyof T = keyof T> = {
    [key in (Keys extends K ? never : Keys)]: T[key]
}
```

第3の引数を型演算の変数に使うというアイディアです。発想力の勝利です。


## その3: 型パズルを頑張る
https://github.com/type-challenges/type-challenges/issues/141

これが一番型パズルっぽい解答でした。まず最初に次のような型を考えます。

```typescript
// Echo<"a"|"b", "b"|"c"> のように使い、Kの値にかかわらずTをそのまま返す
type Echo<T, K> = T extends infer U ? U : never
```

これは何が嬉しいかというと、後でconditional typesを繋げていくとき `T` に `U` という**別名**が付けられる点です。

なお、これはビルトインの `Extract<T, K>` と似ていますが結果はまったく異なることに注意しましょう。 `Extract<T, K>` は `T` と `K` の共通部分を取り出すものです。

```typescript
// 以下3つはすべて "a"|"b" 型
type T1 = Echo<"a"|"b", "c">
type T2 = Echo<"a"|"b", "b"|"c">
type T3 = Echo<"a"|"b", never>

// Extract<T, K>の定義: type Extract<T, K> = T extends K ? T : never
type T4 = Extract<"a"|"b", "b"|"c"> // "b" 型
type T5 = Extract<"a"|"b", "c"> // never 型
```

次に本命の `Exclude<T, K>` について考えます。先ほどの `Echo<T, K>` は `U` を通じて `T` そのものを返しましたが、これを `Exclude<T, K>` を返すように書き換え、さらに定義を展開してみます。

```typescript
type Echo<T, K>       = T extends infer U ? U                       : never
type MyExclude1<T, K> = T extends infer U ? Exclude<U, K>           : never
type MyExclude2<T, K> = T extends infer U ? U extends K ? never : U : never
```

ごちゃごちゃしてきましたが、これは「『 `T` そのものを返す `Echo<T, K>` 』をいじって `Exclude<T, K>` を返すようにした」ものなので、 `Exclude<T, K>` とまったく同じ結果を返すことが分かります。

```typescript
type T6 = MyExclude2<"a"|"b", "b"|"c"> // "a"
type T7 = MyExclude2<"a"|"b", "c"> // "a"|"b"
```

嬉しいことに、最終結果には `T` ではなく `U` が使われています。つまりこいつを `Exclude<T, K>` の代わりに突っ込んでやれば……

```typescript
type MyOmit<T, K> = {
    [P in Exclude<keyof T, K>]: T[P]
}

//　↓

type MyOmit<T, K> = {
    [P in MyExclude2<keyof T, K>]: T[P] // Type 'P' cannot be used to index type 'T'.
}
```

エラーになってしまいました。TypeScriptは `T` と `MyExclude2<keyof T, K>` の結果との関係を理解できないようです。

そこでもうひとひねりして、最後の `U` の代わりに `Extract<U, T>` を入れてやります。もともと `U` は `T` の部分集合なので、 `Extract<U, T>` は `U` そのものを返します。つまり `Extract<U, T>` は何も仕事をしないのですが、TypeScriptはこれで `U` と `T` との関係を理解できるようになります。

```typescript
type MyExclude2<T, K> = T extends infer U ? U extends K ? never : U             : never // これだとダメなので
type MyExclude3<T, K> = T extends infer U ? U extends K ? never : Extract<U, T> : never // こうしてやる

type MyOmit<T, K> = {
    [P in MyExclude3<keyof T, K>]: T[P] // 通った!!
}
```

最後に `MyExclude3<T, K>` と `Extract<U, T>` の定義を展開してやれば終わりです。

```typescript
type MyExclude4<T, K> = T extends infer U ? U extends K ? never : U extends T ? U : never : never // 展開
type MyOmit<T, K> = {
    [P in (keyof T extends infer U ? U extends K ? never : U extends keyof T ? U : never : never)]: T[P]
}
```

[【Playground】](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoHsAm7kG8BQyRyYwYANhAFzIDOYUoA5ocZhLQowA6nog16jEC2LIE6ALbdKkTDQBG6dJTgh8AX3z4A9DuQBRBAAt0AHgBEcCwB8LCiwBpk92xYQWAfMkB2DIAiGQDEGQGsGQH95QBEGQEAGAGkfQBIFIMBpBgTAeIZASIZALQZUQCSGQF0GH0A-BnzAFfjATQZ8MABPbhQjUzNUZyjvAF40ZAgAD0gQTFpkUBhoZABVZAB+EeQaEAgAN2gAbm09ZEBTuUBoOQBmQBMGQHsGEsBPBkAzBhdrOwdkQGj1cqqUVABGZFba8ys3B2d3LxvqtAAmJ6GEyvM6uT4OOweTw-O6bQEvSygj7IGbzKDQ3T6AAUgAubACUyEAjoqALATAIGRgAJfLKANeVAFEMgC-FEqGLpQRBgeqNbyACwZdoBVBICVExlV+BmZrPZyCagNQ7S6EB6fSi4za0zmixhaAALPDRQg2W9zk4XBCvt4Vq4rurUABWbUMMX6sEuKHIFaoobXZb6BENCWeQAyDMMsc1UHjAOYMu0AVgyAeQYRQhyABXdhmYYcrKlQJBQDqDIB9BnVAFkKrGE+x7uLJa1pZ1ur1+iBBlBJhMi4mIMmOVMUaqoEshSgC832H8yy02lW5TWBkNRhNRmP5RKlW6GzRRiq0QsXV6OnGWz5AFnagEr-LKAVR1ANGpOc9yH72+LECHPqauwHrYfnk5+UAxgzZqOAGBVABoMgAKvoAOeZpmUvZoAAbIC147oOiLvIariQl4G5mtYloAOzQYWN4tkODrIiaqH6ARFiXsM4bRiKdq6m2aCeKBGYXuBMG3psw5SjK1Z9JODbTpMc41oqExLh21EsrRKb0R2S4bvmOGwRAGocRWXHjjxdZTkqs6yvOwmdmiHY6dxbQzjJXbmeum5MjRYD7keZ4XvJADykhkCpeDWc5UQAISsEQADaAAKtbIFiADWEAVOgMCjrpE6aXx2lqXpi4WSuKU1pF0WxdKZlrkMBVQHiAC6NCoMFJWaNo4GmEwKCtAWrnuRg2CfKQFAQN84EwPGTBwNhzVsq16CfOwnA8HwIBuB0s3dbcyDcMA0WDW5w1YKNLgdZQbjjVwwC8MA-BuBI0iyBAmDfEAA)

# まとめ
2021年も頑張ります💪