(2017/12/27 追記)
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
性質:
P1
がP2
のサブタイプである ⇒F2
がF1
のサブタイプである
この性質は 反変性 と呼ばれる. (F1
, F2
に対して P1
, P2
の関係が逆転していることに注意)
具体的な例では,
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 では関数型をパラメータに関して反変, 戻り値に関して共変としている.
すなわち,
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
- 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
この記事は CC BY 4.0 (クリエイティブ・コモンズ 表示 4.0 国際 ライセンス) の元で公開します。