Open7

TypeScriptでOmitが使えないパターン

たふみたふみ

Omitとは、TypeScriptでオブジェクトから特定のプロパティを削除するUtility Typeである。

type A = {
  a: number
  b: string
}

type B = Omit<A, 'a'>

const b: B = {
  a: 8, // エラー
  b: "hello",
}
たふみたふみ

このOmitには大きな問題がある。

type A = {
    a: number
} & (
    {
        b: number
        c?: never
    } | {
        b?: never
        c: number
    }
)

このような型を考える。これはbが指定されているときはcが指定できなく、また逆も然り、という型である。これを使うと、

// Type '{ a: number; b: number; c: number; }' is not assignable to type 'A'.
//   Type '{ a: number; b: number; c: number; }' is not assignable to type '{ a: number; } & { b: never; c: number; }'.
//     Type '{ a: number; b: number; c: number; }' is not assignable to type '{ b: never; c: number; }'.
//       Types of property 'b' are incompatible.
//         Type 'number' is not assignable to type 'never'.
const a: A = {
    a: 8,
    b: 8,
    c: 8
}

これをエラーにできる

たふみたふみ

このとき、aプロパティを削除したいとする。

普通はこれを

type B = Omit<A, 'a'>

と書く。しかしこれではBが

type B = {
    b: number;
    c: number;
}

となってしまい、結果的に、

// No Error!
const b: B = {
    b: 8,
    c: 8
}

これが許されてしまう

たふみたふみ

これはOmitの型が

type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }

で表現されていることに由来する。取り除きたいプロパティ名を与えると、それを除いたkeyのunionを作り、そのプロパティのみで構築されるオブジェクト型を再構築している。このときにAの型を作る際に使った &| の表現が保持できない。なぜならシンプルに T[P] という型で表現するため、プロパティ間の依存が取り除かれてしまうからである。

たふみたふみ

解決策があった

type A = {
    a: number
} & (
    {
        b: number
        c?: never
    } | {
        b?: never
        c: number
    }
)

type MyOmit<T, K extends keyof T> = {
  [Property in keyof T as Exclude<Property, K>]: T[Property];
};

type B = MyOmit<A, 'a'>

// Type '{ b: number; c: number; }' is not assignable to type 'B'.
//  Type '{ b: number; c: number; }' is not assignable to type 'OmitX<{ a: number; } & { b?: undefined; c: number; }, "a">'.
//    Types of property 'b' are incompatible.
//      Type 'number' is not assignable to type 'undefined'
const b: B = {
    b: 8,
    c: 8,
}