Union型の別の型にあるプロパティを使ったらエラーにしたい

TypeScriptには複数の型のいずれかをとるUnion型というものがあります。

```ts
interface AB {
    a: string;
    b: number;
}

interface BC {
    b: number;
    c?: boolean;
}

type ABorBC = AB | BC;
```

という型があるとき

```ts
const ab: ABorBC = {
    a: 'abc',
    b: 123,
};
const bc: ABorBC = {
    b: 123,
    c: true,
};
```

のように同じ型`ABorBC`でも違うプロパティの組み合わせを持つことができます。

しかしこの`ABorBC`型は以下のように

```ts
const abc: ABorBC = {
    a: 'abc',
    b: 123,
    c: true,
};
```

と書いてもエラーになりません。

これを解決するための方法が[お互いの type を union type としますが、お互いの property を never にしたいとき](https://qiita.com/dkdidudtn/items/56f1565b6048ff6091ec)に書いてありました。

```ts
const abc2: ABorBC | Partial<Record<keyof AB, never>> | Partial<Record<keyof BC, never>> = {
    a: 'abc',
    b: 123,
    c: true,
//  ^ Object literal may only specify known properties, and 'c' does not exist in type 'AB'.
};
```

※正確に言うと書いてある内容とちょっと違いますが意味は同じってことで。

## 法則を推測

ところで、なぜこれでエラーになるんでしょうか?単なるUnion型と何が違うんでしょう?

正確なところは分からないままなんですが、`NonEmpty<AB> | NonEmpty<BC>`を展開してから削って最小の記述にしてみました。

```ts
const abc3: ABorBC | { a?: never } = {
    a: 'abc',
    b: 123,
    c: true,
//  ^ Object literal may only specify known properties, and 'c' does not exist in type 'AB'.
};
```

プロパティ`a`がオプショナルでnever型なオブジェクト型を指定することで、プロパティ`c`でエラーが出るようになっています。

`a`を`b`や`c`に置き換えるとどうなるでしょう。

```ts
const abc4: ABorBC | { b?: never } = {
    a: 'abc',
    b: 123,
    c: true,
};
// -> エラーにならない

const abc5: ABorBC | { c?: never } = {
    a: 'abc',
//  ^ Object literal may only specify known properties, and 'a' does not exist in type 'BC'.
    b: 123,
    c: true,
};
```

`b`だとエラーにならず`c`だと`a`のところでエラーになりました。

つまり

|`{X?:never}`の`X`の部分|結果|型推論|
|-|-|-|
|`a`|エラー|`AB`|
|`b`||不明|
|`c`|エラー|`BC`|

`b`のときの型推論はエラーになっていないので分かりませんが、エラーになっていないという結果は`ABorBC`つまり`AB | BC`と同じです。ここから

- 指定されているプロパティと同じ名前でオプショナルなnever型であるプロパティを持つ型がUnion型の1つにあれば、Union型の中からその名前のプロパティを持つ型だけが型推論の対象となる

のではないかと推測しました。また

```ts
const abc6: ABorBC | { a?: never; b?: never; c?: never } = {
    a: 'abc',
    b: 123,
    c: true,
//  ^ Object literal may only specify known properties, and 'c' does not exist in type 'AB'.
};
const abc7: ABorBC | { a?: never; b?: never; c?: never } = {
    c: true,
    b: 123,
    a: 'abc',
//  ^ Object literal may only specify known properties, and 'a' does not exist in type 'BC'.
};
```

となることから、オプショナルな`never`型を持つ型は1つでよくて、型推論の対象になるのはオブジェクトリテラルでのプロパティを記述した順序によることが推測されます。

## 空オブジェクト対策

ただ、オプショナルな`never`型は型推論を制御するためだけの型なので、どんな型とも合致しない型である必要があります。`{ a?: never; b?: never; c?: never }`のようにすべてオプショナルだと`{}`と合致してしまうので一工夫必要です。

そこで

```ts
declare const absence: unique symbol;
```

というシンボル宣言(だけ)を用意して、決して存在しないプロパティを追加してやります。

```ts
const abc8: ABorBC | { a?: never; b?: never; c?: never; [absence]: never } = {
    c: true,
    b: 123,
    a: 'abc',
//  ^ Object literal may only specify known properties, and 'a' does not exist in type 'BC'.
};
const empty3: ABorBC | { a?: never; b?: never; c?: never; [absence]: never } = {};
//    ^^^^^^ 
```

こうすれば、空オブジェクトでも安心。

## 型関数化

これから型関数を作ってみました。

```ts
type Unexistent<T> = T & {[absence]: never};

type PreventPropertyOfAnotherType<T> =
    | T
    | Unexistent<Partial<Record<T extends T ? keyof T : never, never>>>;
```

この型関数を使って、型引数にUnion型を渡してやると

```ts
const abc9: PreventPropertyOfAnotherType<ABorBC> = {
    a: 'abc',
    b: 123,
    c: true,
//  ^ Object literal may only specify known properties, and 'c' does not exist in type 'AB'.
};
const empty4: PreventPropertyOfAnotherType<ABorBC> = {};
//    ^^^^^^ Type '{}' is not assignable to type 'PreventPropertyOfAnotherType<ABorBC>'.
```

違う型のプロパティを使おうとするとエラーになり、空オブジェクトは受け付けない型ができました。

## この記事のコード

[Playground](https://www.typescriptlang.org/ja/play/?target=99&jsx=0&exactOptionalPropertyTypes=true&ts=5.5.3&ssl=87&ssc=1&pln=101&pc=1#code/JYOwLgpgTgZghgYwgAgIICFkG8CwAoZQ5OALmQGcwpQBzAbnyOQCMyQBXAW2egbwF98+UJFiIU6AMLZGRVsg7deswggD8ZZgHstAGwhwQfQXnxgAngAcUGLVCnIAvGkwAfZFL74EWkJWLytvbSzrgERKTIAORwzAhRADQqLGQAjABMAMxJAnw+fmAsCGRBDqHJ8hnZycXIVOwQOfxeePn+sbWlITLhhJExcYkVaVk5TLX1jfjNQq2+7XHpJeh2Du4ACnBQYMBwugA8AEoQPlAAJvsA1hDmWjAuCQoQAG7QAHxvyBtbO3tHJ3YLtdbvcpI8QC93p9yr1iGQBvExnIRtVYRMoA0cgB6LGEAB6yAA8swAFYnQq6YCiPbIThwczIXy6BnkawIYAwBmXEBaADuIGQligWms22AEHIj0MZ2i8WQZy0EoUWkKEAAHsB-KA6lYUFEMFEAHTTFptVWcSwWZaraTfMV-Y6nIE3O4PJ6vKAfL7ITb2g6OwFXF2gyTgyGe6HYGZ4HHIQAVDIBLhkAPwyAawZAFYMgEiGNOAEQZZhZrMgAHK+ACiFos+wAKpGsABtADSyG1wNdlYAumQA+d9vWwx7PgAyH0-Xb+gHdktqhC6dhnCBBkHISuPetvPtQ-i1lv3dum+bmy3mJYuG3e4sgMuH-YYT7uc+XitSGvR2NEPHvj9L3XRLD8KJN8hlUKOByHIYAaBAWJ9DqLQdQLfUVmCM9S3Lcxr3QW8ixQq8nyNWYzQCBBMmtJD3CwYgNHdaBkH4JweiYfoOiGWFKlGGoyEmbFcWQAliTJBAKSpaAaTpBkmRZNkOS5Hl+UFYVRR2CUpRAGUojlBUlR5VUNS1AV8z1A1jVyfD90IgAWEi1mwFhKIhD0aLosIGPhJikUIVjUXGDiMSmYyY1xRNU0zHNvFMjoAFZLNtaz1DYcMHJhZzolc-BX140lyWQSlqV0Wl6UZEBmQoSTOWQbk+QFIURWgRTJWIFTkv-DTAK05B1U1QptX06IpDwliUTc5B0UxE0TIKQiADYou9ci4Fs8M6BsuKPUW2KqKgBL6IiFzBkGjzBuG3y0qJDKBKyoSoBE-LxOKk4pLKmTKvkmrxTq6VZSaxUWpVNqdM6vTvwQvrowIjoAHZprIijlt4Jb1tW+b7NoxKiEOvaBuSRjdtS7j0v4wScrysTCoku7SvK2SqoU17lNUuBPs0n72t0uC9V6ozo3wOdpy2FBQeYcgIBAJAyHYEBgAARwaChzG4PQ93GjoAA5IesuaYagRbmER2G1rs2Ha1iQXhYgDt1s2pzUe8kb+uQKpBqxxEcfxE78fOwnRIKorWTJ6SKrk6qxSU+q6YZ77tI6psAfg9nRrmcaIFQ4iT1ItWdc1uH9YzvWFuQQ2BaFpAzazi2X24-EP3fL94N-f9NSA4hQPAyDmGgsBYO6hDTyh9X4cz3Oc5WvOjcL02NcWv8jLMb8AFUIT+oWwCrSNK2QIc6xHk3i-DTm8G69YoEhcAD8DixCRgVAtIAC2gStdWXpxkncSsn+QOfmcgcB9l9X4xydKtfqfzOIBVeagyrBiXMgDW64IxvAVgsBAABOMgB8j5gBPgpcw59L4qhvlAO+1h0I2hrJjHaTtbb23YnUHyXEXZ8UytlYSuVPY3R9uycmj0A7U2Du9NSYcG4fyjqzaIhk44EUToeCyPpD6vGPs9bYWCL7X1vvfLoz4+CvgrpXaueosAAB0-wAQbiBMCEEoIoHbsIqIqDZHoPkWfJRuCVGELUX1IAA)

## 注意点

これをさらにPlaygroundでいろいろ試している内に、これが5.0以前では効かないということが分かりました。

5.1での変更点をUnionで検索すると[Negative Case Checks for Union Literals](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-1.html#negative-case-checks-for-union-literals)しか見つけられませんでした。

これが意図的な変更でない場合、TypeScriptのバージョンアップに伴いバグとして修正されてしまって効果がなくなる可能性もあります。ご利用の場合は注意してください。

## 最後に

この記事は [お互いの type を union type としますが、お互いの property を never にしたいとき](https://qiita.com/dkdidudtn/items/56f1565b6048ff6091ec) をきっかけに気付いたことを書いています。

きっかけとなった記事を書いてくださった @dkdidudtn さんに感謝。