角待ちは対空

おもむろガウェイン

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[] のサブタイプであることはそのままです。

TypeScriptの`Object`型と`object`型と`{}`型の使い分けについて

TypeScriptには似たような型としてObject型とobject型と{}型が存在します。

let o1: Object;
let o2: object;
let o3: {};

今回はこの3つの使い分け、あるいはobject型導入の経緯についてです。

JavaScriptのデータ型

JavaScript のデータ型とデータ構造 - JavaScript | MDNを読めば分かるように、

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol

の6種のプリミティブ型を持つプリミティブ値とオブジェクトでJavaScriptは成り立っています。

TypeScriptにおけるobject型とはここでいうプリミティブ型以外を表現しています。

object型はいつ使われるのか

いつ役に立つかというとObject.create()の定義です。

TypeScript/lib.d.ts at master · Microsoft/TypeScript · GitHub

/**
  * Creates an object that has the specified prototype, and that optionally contains specified properties.
  * @param o Object to use as a prototype. May be null
  * @param properties JavaScript object that contains one or more property descriptors.
  */
create(o: object | null, properties: PropertyDescriptorMap & ThisType<any>): any;

Object.create()の第一引数はobject | nullです。したがってObject.create('str')Object.create(4403)などはエラーとなります。

Object型とは何か

これは単純にObjectオブジェクトの構造を記述するためのInterfaceです。

追記

Objectオブジェクトの型はObjectConstructorなので、Object型は全てのオブジェクトに共通する構造を記述するInterfaceといったほうが正しかったです。

{}型とは何か

これは{}のようなオブジェクトリテラルで書かれる、プロパティを持たないオブジェクトの型を表現しています。ただしJavaScriptなので{}オブジェクトはプロトタイプチェーンの結果Objectオブジェクトのメソッドやプロパティが利用できるはずなので、結果として{}型とObject型は実質同じ型となっています。

使い分け

というわけで普通我々が使う場合は大体{}型です。

オブジェクト共通の構造を示したい場合はObject型を使うのが良いんだと思いますがシチュエーションは思い浮かびません。

object型を使うのはプリミティブ型ではいけない場合です。つまりどういうことは導入の経緯とともに見ていきます。

object型導入の経緯

プリミティブ型でもObject型に定義されたメソッドやプロパティが使える

JavaScriptの仕様上true.toString()のなどの呼び出しが可能です(trueはプリミティブ値なのにObjectオブジェクトのメソッドを呼んでいる)。何故これが可能かと言えば暗黙的にオブジェクトが生成されているからです。つまりtrue.toString()(new Boolean(true)).toString()と同等です。

この暗黙的生成を表現するためTypeScriptではプリミティブ型はObject型の構造も含んでいます。

この仕様で起こる問題

TypeScriptはStructural subtypingを採用しているので、Object.create()の型定義ができないことになります。

つまり

create(o: Object | null, properties: PropertyDescriptorMap & ThisType<any>): any;

としてしまうとObject型を満足する変数やリテラルを引数に取れることになります。すると例えば"str"(string型)は先ほど説明した仕様によりObject型を満足させられるのでObject.create("str")はTypeScript上ではエラーとして検出されません。JavaScriptとして実行された時初めてエラーになります。

これがobject型という非プリミティブ型を表現する型が導入された経緯です。

使い分けないと困るのか

object型が導入された経緯みたいな話はありますが、{}型とObject型に関して言えばどっち使っても極端に困ることないと思います(ただしIDEの補完については多少差がでます)。気分の問題と言えば気分の問題ですが似たようなものがある意味くらいは知っておく良いかと思います。