角待ちは対空

おもむろガウェイン

TypeScript 2.4のSafer callback parameter checkingについて

TypeScript 2.4 RCがリリースされました。

Announcing TypeScript 2.4 RC | TypeScript

いくつか変更点があるのですがこのエントリではSafer callback parameter checkingについて解説します。公式ドキュメントでいうとFAQ · Microsoft/TypeScript Wiki · GitHubあたりの話に関連します。あるいはなぜ TypeScript の型システムが健全性を諦めているかとも関連します。

Dog[]Animal[] のサブタイプか

TSでは DogAnimal のサブタイプである時、Dog[]Animal[] のサブタイプです。型システムとして健全かどうかは置いといて便利なのでこうなっています。

さて、Dog[]Animal[] に代入可能かどうかを判定する際コンパイラは最終的に、(x: Dog) => number(x: Animal) => number に代入可能かを調べることになります。ではこれをどうやって判定するのでしょうか?答えは「DogAnimalに代入可能もしくはAnimalDogに代入可能」かで判定します。

つまり「DogAnimalに代入可能もしくはAnimalDogに代入可能 ならば(x: Dog) => number(x: Animal) => numberに代入可能」ということです。

閑話

本筋とはズレますが、Dog[]Animal[] のサブタイプとしたときに、型システムとして健全性が崩れる例です。

class Animal {
}

class Dog extends Animal {
    bark(): void {}
}

let a = [new Animal]
let d = [new Dog]

a = d;


a[0] = new Animal

for ( let item of d) {
    item.bark()
}

// => Uncaught TypeError: item.bark is not a function

この仕様の問題点

Dog[]Animal[] のサブタイプであることの問題点はさておき、「DogAnimalに代入可能もしくはAnimalDogに代入可能 ならば(x: Dog) => number(x: Animal) => numberに代入可能」となることが問題になります。

アナウンスブログからの引用でいうと

interface Animal { animalStuff: any }
interface Dog extends Animal { bark(): void }

interface BasicCollection<T> {
    forEach(callback: (value: T) => void): void;
}

declare let animalCollection: BasicCollection<Animal>;
declare let dogCollection: BasicCollection<Dog>;

// This should be an error, but TypeScript 2.3 and below allow it.
dogCollection = animalCollection;

dogCollectionanimalCollectionが代入可能かどうかは最終的にはインターフェースBasicCollectionforEachの引数になっているcallback部分が代入可能化どうかで判定されます。つまりcallback: (value: Dog) => voidcallback: (value: Animal) => voidが代入可能かどうかですが、配列の例で見た時と同じロジックで代入可能と判断されdogCollection = animalCollectionはエラーになりません。

具体的にcallback

dogCollection.forEach((value: Dog) => {value.bark()});

だと想定すると実行時にエラーになるのがわかると思います。

これはPromise<T> におけるthenにも当てはまりますので、Promise<Animal>Promise<Dog> に代入可能なことになってしまいます。

2.4からどうなるのか

callback関数の判定時は特別に(x: Dog) => void(x: Animal) => void に代入可能かの判定がAnimalDogに代入可能かで判定されるようになります。

というわけでdogCollection = animalCollectionはエラーとなり、animalCollection = dogCollectionは(引き続き)エラーになりません。またPromise<Animal>Promise<Dog>に代入することもできなくなります。

TS 2.4以前(playdroundがアップデートされるまではエラーが出ない様子が見れると思います)。

2.4以降は以下。

interface Animal {

}
interface Dog extends Animal {
    someProperty: string
}

let a: Promise<Animal>;
let d: Promise<Dog>;

d = a;

// =>
// a.ts(11,1): error TS2322: Type 'Promise<Animal>' is not assignable to type 'Promise<Dog>'.
//   Type 'Animal' is not assignable to type 'Dog'.
//     Property 'someProperty' is missing in type 'Animal'.

まとめ

Dog[]Animal[] のサブタイプにするためにPromise<Animal>Promise<Dog>に代入可能でしたが、2.4からはPromise<Animal>Promise<Dog>に代入できなくなります。Dog[]Animal[] のサブタイプであることはそのままです。