なぜ TypeScript の型システムが健全性を諦めているか

(2017/12/27 追記)



```ts
class Animal { kind: string }
class Dog extends Animal { dog_type: string }

function showDogType(dog: Dog) { console.log(dog.dog_type) }

// 代入不可のコンパイルエラーになってほしい
const someAnimalFunc: (animal: Animal) => void = showDogType

// 実際はここでランタイムエラー
someAnimalFunc(new Animal())
```

このコードがコンパイルエラーにならず, 実行時エラーになってしまうことについて.

# TL;DR

- 型システムの健全性 (型の矛盾が生じないこと) vs 利便性のトレードオフから, 現在の TypeScript は健全性を諦めている
    - 関数型はパラメータについて bivariant (共変かつ反変), 戻り値について共変としている
- 健全性を得るための提案 (関数パラメータについて反変とする) が議論されている

# 関数パラメータに関しての性質: 反変性

一般に関数型には次の性質が求められる.

```ts
type F1 = (x: P1) => void
type F2 = (x: P2) => void
```

> 性質: `P1` が `P2` のサブタイプである ⇒ `F2` が `F1` のサブタイプである

この性質は **反変性** と呼ばれる. (`F1`, `F2` に対して `P1`, `P2` の関係が逆転していることに注意)

具体的な例では,

```ts
type DogFunc = (x: Dog) => void
type AnimalFunc = (x: Animal) => void
```

`Animal` 型の入力を期待する `AnimalFunc` を `DogFunc` 型に代入しても問題なく動作するべきである. 一方で `Dog` 型の入力を期待する `DogFunc` を `AnimalFunc` に代入できてしまうと, `Dog` 型以外の `Animal` サブタイプ (`Cat` 型とか) を渡される可能性があり, 矛盾が生じる.

# 共変な Array と共変な関数パラメータ

ところで, `Dog[]` は `Animal[]` のサブタイプとみなすべきだろうか. 現在の TypeScript は `Dog[]` を `Animal[]` のサブタイプと **みなしている**. つまり以下の性質を認めている.

> 性質: `P1` が `P2` のサブタイプである ⇒ `P1[]` が `P2[]` のサブタイプである

この性質は **共変性** と呼ばれる.

`Dog[]` が `Animal[]` のサブタイプであるか否かを判定する際, コンパイラは次のような判定を行う.

- `Dog[]` は `Animal[]` 型に代入可能か?
    - `Dog[]` の全てのメンバ (プロパティ, メソッド) が `Animal[]` 型に代入可能か?
        - `Dog[].push` メソッドは `Animal[].push` 型に代入可能か?
            - 関数型 `(x: Dog) => number` は `(x: Animal) => number` に代入可能か?

ここでさっきの議論を思い出す.

> 関数パラメータの反変性: `Animal` を `Dog` に代入可能 ⇒ `(x: Dog) => number` を `(x: Animal) => number` に代入可能

`Animal` は `Dog` に代入可能ではない. 関数パラメータの反変性が成り立つとすると, 「型 `(x: Dog) => number` は `(x: Animal) => number` に代入可能か?」の答えは no となる. すなわち「`Dog[]` は `Animal[]` 型に代入可能か?」の答えは no となってしまう.

そこで TypeScript では関数パラメータを **bivariant** (共変かつ反変) とすることでこれを解決している.

つまり以下の性質を許している.

> `Animal` を `Dog` に代入可能 **もしくは** `Dog` を `Animal` に代入可能 ⇒ `(x: Dog) => number` を `(x: Animal) => number` に代入可能

この性質は型システムの健全性を損ない冒頭コードのような矛盾を生むが, TypeScript では Array 型を共変にするためのトレードオフとしてこの矛盾を飲んでいる.

# 健全性: Flow の例

Flow では関数型をパラメータに関して反変, 戻り値に関して共変としている.

すなわち,

```ts
type F1 = (x: P1) => R1;
type F2 = (x: P2) => R2;
```

P1 が P2 のサブタイプかつ R2 が R1 のサブタイプならば, F2 は F1 のサブタイプである, としている.

その結果, 健全性は保たれる一方で, Array の共変性を諦めている. (`Dog[]` を `Animal[]` 型に代入できない)

# 健全性: TypeScript での議論

実は 2014 年から関数パラメータを反変とするべきという提案が [議論されている](https://github.com/Microsoft/TypeScript/issues/1394#issuecomment-250319666).

- エンドユーザにとって共変 / 反変の概念の理解が難しいこと
- 健全ではないが実用的なパターンが多いこと
- Array の共変性を捨てなければならないこと

などを理由に受け入れられていなかったが, `Promise` 型の不整合 (後述) などの大きな問題から, 状況が徐々に [変化している](https://github.com/Microsoft/TypeScript/issues/1394#issuecomment-250319666).

## `Promise` 型の不整合

ごく単純化すると以下のような問題が生じている.

```ts
interface Promise2<T> {
    then<U>(cb: (value: T) => Promise2<U>): Promise2<U>;    
}
var animal: Promise2<Animal> = null as any
var dog: Promise2<Dog> = null as any

animal = dog // OK (then の反変性)
dog = animal // NG にしたいが, then の bivariance のためコンパイルが通る
```

# Link

- Why are function parameters bivariant? | FAQ · Microsoft/TypeScript Wiki https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant
- Flow | Variance https://flowtype.org/docs/variance.html#function-types
- Proposal: covariance and contravariance generic type arguments annotations · Issue #10717 · Microsoft/TypeScript https://github.com/Microsoft/TypeScript/issues/10717
- Covariance / Contravariance Annotations · Issue #1394 · Microsoft/TypeScript https://github.com/Microsoft/TypeScript/issues/1394

[![クリエイティブ・コモンズ・ライセンス](https://i.creativecommons.org/l/by/4.0/88x31.png)](http://creativecommons.org/licenses/by/4.0/deed.ja)
この記事は [CC BY 4.0 (クリエイティブ・コモンズ 表示 4.0 国際 ライセンス)](http://creativecommons.org/licenses/by/4.0/deed.ja) の元で公開します。