TypeScriptにおけるobject typeのunionの、あまり期待されてないのではないかと思われる挙動について

サンプルコード

'use strict';

type A = { a: string }
type B = { b: number }
type C = { c: boolean }
type T = A|B|C

const v1: T = { a: '1', b: '1', c: '1' }
const v2: T = { a: 1, b: 1, c: 1 }
const v3: T = { a: true, b: true, c: true }
const v4: T = { a: false, b: '0', c: 0 }

何が「期待されてないのではないかと思われる挙動」なのか

ここで(v4ではエラーになるのはともかく)、v1〜v3に代入している値は、型 A, B, C のどれにも代入できないにもかかわらず、それらの union である A|B|C 型には、コンパイル時のエラーにならず、代入できてしまう。これは、(静的な)強い型付けを期待しているプログラマの想像に、多分、反している。

仕様ではどう言っているのか

仕様 ( https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md ) では(以下、2018年4月現在のバージョンである Version 1.8 を前提とする)、§3.4 Union Types に、次のようにある。

A type T is assignable to a union type U if T is assignable to any type in U.

(訳: 型Tの値は、union型Uに、Uのどれかの型に代入可能である場合に、代入可能である。)

ここで、次のようではないのは意図的なものなのではないか、と思われる。

***NOT*** / A type T is assignable to a union type U if and only if T is assignable to any type in U.
(訳(こうなってはいない): 型Tの値は、union型Uに、Uのどれかの型に代入可能である場合、かつその場合に限り、代入可能である。)

つまり、このあたりの挙動については、型によって安全を保証することを諦めている(のではないか)と思われる。理由としては、最初に結論として述べたように、Discriminated Union を使えば、こういった問題は避けられるからである。

想像される実装

以下は tsc の実装を読んだわけではなく、いくつかの例で試した結果からの想像である。

複数個の object type のある Union型 U への object 型 T の代入可能かのチェックでは、

  1. まず、 T の属性名の中に、U の型のどれにも現れない属性名がないか確認する。どれにも現れない属性名があったら代入できない。この時、その属性の名前のみがチェックされ、型はチェックされない。
  2. 次に、U の型について、左から順番に、T が代入可能かどうかについて試してゆく。この時、通常の代入可能のチェックとは異なり、T の側に属性が余ってもエラーにはしない。
(これで、前述の仕様は満たされているはずです。また、以上の規則で説明できない例が確認できましたら、よろしければコメントください)