30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

TypeScript の共用体型(Union Types)は or ではない(追記あり)

〜2020/7/8 追記〜

本記事に対して指摘を頂いた。

qiita.com

「余剰プロパティチェックの存在やそのルールによってorではないかのように見える(ことがある)というだけで、型システム的にはorである」と、私は解釈した。

特に「余剰プロパティチェックは、型システムに対する違反を検出するものではなく言わば追加の親切なチェック」という点が重要であり、確かにそこを無意識のうちに混同していた。

分かりやすく説明して頂いているので上記の記事を読めば十分かもしれないが、勉強も兼ねて、自分でも整理しておく。

共用体型は or である

TypeScript では、オブジェクトに余計なプロパティがあっても問題にならない。
以下の例だと、string型のaというプロパティが存在していればAの条件を満たしていることになり、余計なプロパティが存在してもエラーにはならない。

type A = {
  a: string;
};

const aAndS = {
  a: "a",
  s: 1,
};

const aAndT = {
  a: "a",
  t: true,
};

const sAndT = {
  s: 1,
  t: true,
};

let expectA: A;

expectA = aAndS; // ok
expectA = aAndT; // ok
expectA = sAndT; // プロパティ a が存在しないため、エラーになる

そのため、以下のaAndXCに割り当てることができる。

type A = {
  a: string;
};

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

type C = A | B;

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

const expectA: A = aAndX; // ok
const expectC: C = aAndX; // ok

aAndXAの条件を満たしているため、「AもしくはBである」というCにも、割り当てることができる。
つまり、A | BA or Bであり、「共用体型はor(日本語の「もしくは」)である」と言ってよい。

「共用体型はorである」という話についてはここで終わりなのだが、「余剰プロパティチェック」の存在によって、話はもう少し複雑になっていく。

余剰プロパティチェック

先程、「余計なプロパティがあっても問題にならない」と書いたが、これには例外がある。
オブジェクトリテラルを型に割り当てようとしたときは、余計なプロパティがあるとエラーになってしまう。

type A = {
  a: string;
};

const aAndS = {
  a: "a",
  s: 1,
};

let expectA: A;

expectA = aAndS; // ok

expectA = {
  a: "a",
  s: 1, // 余計なプロパティ s があるので、エラーになる
};

しかし、冒頭にも書いたが、これは型システムに対するエラーではないのである。
既に書いたように、TypeScript では余計なプロパティがあっても許容するため、型システム上は問題ない。
ただ、オブジェクトリテラルを割り当てる際に余計なプロパティがあれば、それは開発者のミスである可能性が高い。そのため、TypeScript はエラーを出すのである。
この仕組みを「余剰プロパティチェック(excess property checks)」という。
そしてこれは、開発者の利便性のためにチェックしてくれているに過ぎず、「共用体型はorであるか」という型システムの話とは、また異なるレイヤーの話である。

しかし、余剰プロパティチェックの対象が共用体型の場合は、これまでの説明とは矛盾する挙動になる。私は、これが原因で勘違いをしてしまっていた。
CであるexpectC2に対してオブジェクトリテラルを割り当て、そして余計なプロパティであるxがあるはずなのに、エラーにならない。

type A = {
  a: string;
};

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

type C = A | B;

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

const expectA: A = aAndX; // ok

const expectC1: C = aAndX; // ok

// ok
// オブジェクトリテラルを割り当てているのに、エラーにならない
const expectC2: C = {
  a: "1",
  x: 2,
};

xが、Cの構成要素であるBに存在するため、このような挙動になる。もう一方の型に存在するプロパティなら、余剰プロパティチェックの対象にはならないのである。
Bに存在しないプロパティだと、エラーになる。

type A = {
  a: string;
};

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

type C = A | B;

const aAndY = {
  a: "1",
  y: 2,
};

const expectA: A = aAndY; // ok

const expectC1: C = aAndY; // ok

const expectC2: C = {
  a: "1",
  y: 2, // 余計なプロパティ y があるので、エラーになる
};

冒頭の記事によると、互換性の問題でこのような挙動になっているとのこと。

〜追記終わり〜

この記事の内容は、TypeScript のv3.9.5で動作確認している。

TypeScript をよく理解している人にとっては常識なのかもしれないが、共用体型はorではない。つまり、A | Bは、A or Bではない。
当初これを理解していなかったため、なぜ以下のコードがエラーにならないのか分からず混乱した。

type A = {
  a: string;
};

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

type C = A | B;

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

{a: "1", x: 2}Aではないし、Bでもない。
実際、ABに割り当てようとすると、エラーになる。にも関わらず、A | BであるCには割り当てることができるのである。

type A = {
  a: string;
};

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

type C = A | B;

const bar: A = {
  a: "1",
  x: 2, // error
};

const baz: B = {
  a: "1", // error
  x: 2,
};

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

このことから、A | Bは単にA or Bを意味するのではない、ということが分かる。

ABもプリミティブ型であるなら、共用体型をorと認識しても問題ない。

以下のCは「string、もしくはnumber」を意味しており、それ以外の型を割り当てようとするとエラーになる。

type A = string;

type B = number;

type C = A | B;

// ok
const foo: C = "a";

// ok
const bar: C = 1;

// error
const baz: C = true;

// error
const quz: C = {};

問題は、ABがオブジェクト型のときである。

ABもオブジェクトのとき、A | Bはまず、AB、どちらかの条件を満たしている必要がある。
これは分かりやすいと思う。

type A = {
  a: string;
};

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

type C = A | B;

// A を満たしている
const foo: C = {
  a: "1",
};

// B を満たしている
const bar: C = {
  b: 1,
  x: 2,
};

そしてその条件さえ満たしていれば、もう一方の型のプロパティを自由に追加できるのである。

type A = {
  a: string;
};

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

type C = A | B;

// A を満たしたうえで、b を追加している
const foo: C = {
  a: "1",
  b: 1,
};

// A を満たしたうえで、x を追加している
const bar: C = {
  a: "1",
  x: 2,
};

AB、両方のプロパティを全て持たせても問題ない。

type A = {
  a: string;
};

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

type C = A | B;

// ok
const foo: C = {
  a: "1",
  b: 1,
  x: 2,
};

どちらにも存在しないプロパティを追加してしまうと、エラーになる。

type A = {
  a: string;
};

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

type C = A | B;

const foo: C = {
  a: "1",
  b: 1,
  y: 2, // error
};

共用体型の要素の数を増やしても、この法則は変わらない。

type A = {
  a: number;
  b: number;
};

type T = {
  t: number;
  u: number;
};

type X = {
  x: number;
  y: number;
};

type Z = A | T | X;

// ok
const foo: Z = {
  a: 1,
  b: 1,
  t: 1,
  y: 1,
};

// いずれの型も満たしていないので、エラーになる
const bar: Z = {
  a: 1,
  t: 1,
  y: 1,
};

// いずれの型にも存在しないプロパティ q を含めているので、エラーになる
const baz: Z = {
  a: 1,
  b: 1,
  q: 1,
};

// ok
const qux: Z = {
  a: 1,
  b: 1,
  t: 1,
  u: 1,
  x: 1,
  y: 1,
};