Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.
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 (共変かつ反変), 戻り値について共変としている
  • 健全性を得るための提案 (関数パラメータについて反変とする) が議論されている

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

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

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

性質: P1P2 のサブタイプである ⇒ F2F1 のサブタイプである

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

具体的な例では,

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

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

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

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

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

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

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

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

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

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

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

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

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

AnimalDog に代入可能 もしくは DogAnimal に代入可能 ⇒ (x: Dog) => number(x: Animal) => number に代入可能

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

健全性: Flow の例

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

すなわち,

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

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

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

健全性: TypeScript での議論

実は 2014 年から関数パラメータを反変とするべきという提案が 議論されている.

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

などを理由に受け入れられていなかったが, Promise 型の不整合 (後述) などの大きな問題から, 状況が徐々に 変化している.

Promise 型の不整合

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

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

na-o-ys
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした