TypeScriptのunion型はorです　〜union型、構造的部分型、余剰プロパティチェックの話〜

先日、「TypeScriptのunion型はorではない」とする以下の記事が公開されました。

- [TypeScript の共用体型（Union Types）は or ではない](https://numb86-tech.hatenablog.com/entry/2020/07/03/090900)

残念ながら筆者はこの主張に同意できないので、この記事では上記の記事に含まれる問題点を指摘しつつ、正しい結論を導きます。

この記事の目的はあくまで正しい情報を皆さまに届けることであり、上記の記事（以下では「元記事」と呼びます）の筆者を攻撃する意図は無いことをご理解ください。

では、元記事からコードを引用しながら何がまずいのかを見ていきましょう。

## 元記事の主張

元記事の最初のコードを引用します。

```ts:元記事から引用
type A = {
  a: string;
};

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

type C = A | B;

const foo: C = {
  a: "1",
  x: 2,
};
```

`C`は`A | B`と定義されているので、union型の意味が「または (or)」であればこれは「`A`または`B`」であるはずです。ここで、`C`型の変数`foo`に`{ a: "1", x: 2 }`というオブジェクトを代入しても、コンパイルエラーは発生しません。

一方で、このオブジェクトを`A`や`B`に代入しようとするとコンパイルエラーとなります。

```ts
// エラー: Type '{ a: string; x: number; }' is not assignable to type 'A'.
//          Object literal may only specify known properties, and 'x' does not exist in type 'A'.
const foo1: A = {
  a: "1",
  x: 2,
};

// エラー: Type '{ a: string; x: number; }' is not assignable to type 'B'.
//          Object literal may only specify known properties, and 'a' does not exist in type 'B'.
const foo2: B = {
  a: "1",
  x: 2,
}
```

つまり、`C`には「`A`にも`B`にも代入できないオブジェクト」を代入できることになります。これを以って、`C`は「`A`または`B`」ではないとするのが元記事の主張です。

## TypeScriptは構造的部分型を採用している

では、元記事の主張のどこに問題があるのか見ていきます。まず、元記事では`{ a: "1", x: 2 }`を`A`にも`B`にも代入できないとしていましたが、実は次のように別の変数を経由することで`A`に代入できます。

```ts
type A = {
  a: string;
};

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

type C = A | B;

const obj = {
  a: "1",
  x: 2,
};

// エラーが発生しない！
const foo1: A = obj;
```

これは、TypeScriptの型システムが**構造的部分型**を採用しているからです。つまり、`A`という型は「`string`型のプロパティ`a`を持つ」という意味ですが、**`a`以外のプロパティを持つことは禁止していません**。つまり、`a`以外に`b`や`x`があろうが、`A`型の条件を依然として満たしているのです。

よって、`{ a: "1", x: 2 }`を`A`型の変数に代入することは、型システム上まったく問題ありません。であるからこそ、このオブジェクトを`C`に代入できたのです。

つまり、このオブジェクトを`C`型の変数に代入できることは、`C`が`A`または`B`であることと矛盾しません。

## 余剰プロパティチェック

しかし、そうなると別の疑問が発生します。このオブジェクトを直接`A`型の変数に入れようとすると次のようにエラーが発生してしまいます。

```ts
// エラー: Type '{ a: string; x: number; }' is not assignable to type 'A'.
//          Object literal may only specify known properties, and 'x' does not exist in type 'A'.
const foo1: A = {
  a: "1",
  x: 2,
};
```

先ほどの説明からすれば、このオブジェクトを`A`型の変数に入れるのは問題ないはずなのに、コンパイルエラーが発生していることになります。これが誤解の原因となっていると考えられます。

実はこのエラーは**余剰プロパティチェック (excess property checks)**と呼ばれるものであり、**他のコンパイルエラーとは性質が異なります**。というのも、このエラーは**型システムに対する違反を検出しているものではない**のです。言い換えれば、**エラーを出さなくても型システム的には問題がないが、親切心でコンパイルエラーを出してくれている**のです。

このコンパイルエラーは、余剰プロパティ、言い換えれば**無駄なプロパティ**の存在を検出するものです。上のコードの場合、`A`型には`a`というプロパティのみが定義されていますから、`A`型の値を使う側のコードは`a`プロパティにのみアクセスが許されます。実際には`x`というプロパティが存在していたとしても、それにアクセスする手段が（TypeScriptを欺かない限りは）存在しないのです。

つまり、`A`型のオブジェクトにおいて、`x`というプロパティはあっても使われません。決して使われないプロパティをわざわざ書くというのは、プログラマのミスである可能性が高いですね。これが、型システム上は問題ないのにわざわざコンパイルエラーを出す理由です。

### 余剰プロパティチェックが発生するタイミング

余剰プロパティチェックは**オブジェクトリテラル**に対して発生します。そして、そのオブジェクトリテラルに与えられる型があらかじめわかっている状況（この状況をcontextual typeがあると言います）で行われます。次のコードの場合、オブジェクトリテラルは`A`型の変数`foo1`に代入されることが分かっているため、余剰プロパティチェックの対象となります。

```ts
// エラー: Type '{ a: string; x: number; }' is not assignable to type 'A'.
//          Object literal may only specify known properties, and 'x' does not exist in type 'A'.
const foo1: A = {
  a: "1",
  x: 2,
};
```

ところで、余剰プロパティチェックを回避する例を再掲します。

```ts
const obj = {
  a: "1",
  x: 2,
};

// エラーが発生しない！
const foo1: A = obj;
```

上の説明に照らせば、なぜここで余剰プロパティチェックが発生しないのか分かりますね。まず変数`obj`への代入は、余剰プロパティチェックが発生しません。変数`obj`の型が指定されていないからです。この場合、型推論により`obj`は`{ a :string; x: number; }`型となります。

そして、`foo1`に`obj`を代入するところでも余剰プロパティチェックは発生しません。なぜなら、代入されるのがオブジェクトリテラルではないからです。構造的部分型のおかげで、`obj`を`A`型の変数に代入するのはまったく問題ありません（`obj`が持つプロパティ`x`はこの時点で無用の長物となりますが）。

なお、`foo1`に`obj`を代入するときに余剰プロパティチェックを行えばいいのではないかという意見があるかもしれませんが、そうすると構造的部分型が形骸化してしまいますから、それは無理筋です。部分型という仕組み自体はとても有用なものです。

## ユニオン型に対する余剰プロパティチェック

元記事から冒頭の例を再度引用します。

```ts:元記事から引用
type A = {
  a: string;
};

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

type C = A | B;

const foo: C = {
  a: "1",
  x: 2,
};
```

変数`foo`には型註釈があり、オブジェクトリテラルが代入されていますから、余剰プロパティチェックが行われる条件を満たしています。しかし、余剰プロパティチェックによるコンパイルエラーは発生しません。

実際のところ、このオブジェクトは`A`型の条件を満たしますが`B`型の条件を満たしていません（プロパティ`b`が無いため）。そして、`A`型として見れば`x`が余計ですから、頑張れば余剰プロパティチェックによるエラーを出せそうに思えます。

また、ユニオン型に対して全く余剰プロパティチェックが行われないわけでもありません。元記事の筆者が突き止めた通り、最初の例は余計なプロパティが`x`だからこそ余剰プロパティチェックを回避しています。これは、`x`というプロパティが`B`に存在するからです。本来このオブジェクトは`C`の構成要素のうち`A`に当てはまるオブジェクトなので、`A`と見なして余剰プロパティチェックを行なって欲しいところ、`B`側に存在する`x`は見過ごされます。

実際、次のように`A`にも`B`にも存在しない`y`を余計なプロパティとして追加すると余剰プロパティチェックによるコンパイルエラーが発生します。

```ts:元記事から引用（エラーメッセージは筆者が追加）
type A = {
  a: string;
};

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

type C = A | B;

// エラー: Type '{ a: string; b: number; y: number; }' is not assignable to type 'C'.
//          Object literal may only specify known properties, and 'y' does not exist in type 'C'.
const foo: C = {
  a: "1",
  b: 1,
  y: 2, // error
};
```

では、なぜ`x`に対してはコンパイルエラーを発生させないのでしょうか。TypeScriptのバグでしょうか？　答えは否です。以下のissueに、Ryan Cavanaughさんによる説明があります（強調は筆者）。

- https://github.com/microsoft/TypeScript/issues/38024

> This is the intended behavior. The provided value is a valid TYPE_A, and **excess property checking (which should not be interpreted as a type system guarantee) allows properties to be present if they properly match some constituent in the target**. Doing otherwise caused a ton of breaks in real working code.

つまり、これはintended behavior（意図通りの挙動）であり、ユニオン型に対する余剰プロパティチェックが行われる際は、ユニオン型の構成要素のいずれかに存在するプロパティは全て許可されるという仕様になっています。そうなっている理由は最後の一文で述べられています。さらに厳しいチェックを実装すると、後方互換性が激しく崩壊したからのようです（そもそも余剰プロパティチェックは当初から実装されていたものではなく、TypeScript 1.6で最初に実装されて以降徐々に追加・改善されてきたものです）。

### 余談

余談ですが、discriminated union type（リテラル型を用いてユニオン型の構成要素を区別するパターン）の場合はちゃんと余剰プロパティチェックを行うという仕様になっており、これは[TypeScript 3.5のリリースノートに明記されています](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-5.html#smarter-union-type-checking)。しかし、なぜか現状はうまく行われない場合があり、バグとして扱われています。

- https://github.com/microsoft/TypeScript/issues/39050
- https://github.com/microsoft/TypeScript/issues/35890

## まとめ

この記事では、[TypeScript の共用体型（Union Types）は or ではない](https://numb86-tech.hatenablog.com/entry/2020/07/03/090900)という記事に対する反論を述べ、**TypeScriptのunion型は「or」である**ことを主張しました。特に、今回問題になっていた余剰プロパティチェックは、型システムに対する違反を検出するものではなく言わば**追加の親切なチェック**であり、union型の意味という型システム的な概念とは切り分けで考えるべきものです。