TypeScriptにおけるobject typeのunionの、あまり期待されてないのではないかと思われる挙動について
結論
Discriminated Union ( http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions ) を使おう
サンプルコード
'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 の代入可能かのチェックでは、
- まず、 T の属性名の中に、U の型のどれにも現れない属性名がないか確認する。どれにも現れない属性名があったら代入できない。この時、その属性の名前のみがチェックされ、型はチェックされない。
- 次に、U の型について、左から順番に、T が代入可能かどうかについて試してゆく。この時、通常の代入可能のチェックとは異なり、T の側に属性が余ってもエラーにはしない。