LoginSignup
1
2

More than 3 years have passed since last update.

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

Posted at

お正月です。一年の計は元旦にありと言いますし、TypeScriptと仲良くなろうと思って Type Challenges を始めました。

問題

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

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> を使って

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

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

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

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

丸カッコ中のconditional typesは neverkeyof T (引数そのもの)かのいずれかを返すので直感的にも明らかですね。

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

【Playground】

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

その1: asを使う

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です。

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

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

【Playground】

その2: 3つめの変数を使う

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

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

その3: 型パズルを頑張る

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

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

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

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

// 以下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> を返すように書き換え、さらに定義を展開してみます。

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> とまったく同じ結果を返すことが分かります。

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

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

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は TMyExclude2<keyof T, K> の結果との関係を理解できないようです。

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

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> の定義を展開してやれば終わりです。

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】

まとめ

2021年も頑張ります💪

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2