TypeScriptの注目の型関連issue

TypeScript Advent Calendarの4日目。

TypeScriptのロードマップを見てもES6対応以外は "Investigate top-rated feature requests" とか書いてあるぐらいで、GitHub Issuesのコメントのやりとりを見ていても割りと流動的に良い提案があったら取り入れる感じで開発を進めている印象。

ということで、GitHub Issuesからおもしろそうなものをいくつか拾って紹介してみる。

個人的な希望として、TypeScriptにはES6 + 型付けというコンセプトを突き進めて欲しいと思っていて(詳細はこの辺のスライドを参照)、言語機能追加系よりも型関連の強化に期待しているので、そっちが多め。

TypeScript 1.4のおさらい

とはいえ1.4で型関連の重要な機能追加がいくつか入ったのでまずはおさらい(MSDN Blogsの記事 TypeScript 1.4 sneak peek: union types, type guards, and more を読んだ人は飛ばしてOK)。

まず Union types で、string|numberみたいに"AまたはB"という型を定義できるようになった。悲願。

次に Type alias で、型に別名をつけることができるようになった。Closure Compiler Annotationでは@typedefと言われていたやつだ。

この2つの活用例として、TypeScript本体のES6型定義を引用する。

declare type PropertyKey = string | number | Symbol;
interface Object {
    hasOwnProperty(v: PropertyKey): boolean;
    propertyIsEnumerable(v: PropertyKey): boolean;
}

type aliasによってunion typeである型PropertyKeyを定義している。

union typeが無いとそもそもPropertyKeyを定義できないし、type aliasが無いと以下のようにすべての箇所でunion typeを長々と指定する必要があって大変見苦しい。

// type aliasを使わない見苦しい例
interface Object {
    hasOwnProperty(v: string | number | Symbol): boolean;
    propertyIsEnumerable(v: string | number | Symbol): boolean;
}

また、union typeが入ると必然とtype guardが欲しくなる。ので入った。 type guardとは特定の条件文によってunion型の値が持つ型の可能性を狭められる機能。こんな感じ。

var a: string | number;

// この時点ではstringとnumberに共通してあるメソッドしか呼べない。
a.toString(); // ok
a.trim(); // error TS2339: Property 'trim' does not exist on type 'string | number'.

if (typeof arg === 'string') {
  // string型に確定するのでstringのメソッドを呼べる
 arg.trim();
} else {
  // number型に確定するのでnumberのメソッドを呼べる
  arg.toFixed();
}

type guardはstingのようなプリミティブにはtypeof、オブジェクトにはinstanceofが使える。がまだ動きはいろいろ微妙なところがある。

#1003 Singleton types under the form of string literal types

前述のtype guardを強化する提案。typeofinstanceofだけでなく、プロパティの文字列リテラルによるタイプガード。

おそらく、これはASTのパーサーとかを書いてると超絶便利な機能。例えば、

interface IfStatement extends Statement {
    type: "IfStatement";
    test: Expression;
    consequent: Statement;
    alternate?: Statement;
}

/**
 * A labeled statement, i.e., a statement prefixed by a break/continue label.
 */
interface LabeledStatement extends Statement {
    type: "LabeledStatement";
    label: Identifier;
    body: Statement;
}

var s: IfStatement | LabeledStatement;

switch (s.type) {
    case "IfStatement":
        // 型がIfStatementに確定する!
        console.log(s.consequent);
        break;
    case "LabeledStatement":
        // 型がLabeledStatementに確定する!
        console.log(s.label);
        break;
}

typeに書いてある文字はクラス名じゃなくてもユニークに決まれば何でも良い。ユニークじゃない場合はunion型になる。

おそらく他言語の人からは「何でパターンマッチじゃないの」とか言われてしまうと思うのだけど、TypeScriptは実体がJavaScriptなので、コンパイル結果との整合性を考えるとパターンマッチは難しい。

また、TypeScriptで上から下まで作るならいいんだけど、例えばJavaScriptで書かれたesprimaのような既存のASTパーサーが吐き出すAST定義はクラスベースではなくてtype: "HogeType"のようなプロパティベースで実装されているわけで、そのASTの利用者側のライブラリをTypeScriptで書きたいと思ったらこういうやり方しか無いと思う。

もし実現したら、自前で型キャストする必要もなくなるっていうか、条件文書いてドット打ったら勝手に正しいプロパティが補完されるなんで最高じゃないですか!

#1007 Support user-defined type guard functions

isキーワードを導入してtype guardを自分で制御していこうという試み。

function isCat(a: Animal): a is Cat {
  return a.name === 'kitty';
}

var x: Animal;
if(isCat(x)) {
  x.meow(); // OK, x is Cat in this block
}

たしかにES6ではArray.isArray()とかもあるから、ある程度は必要そう。

Closure Compilerの場合、goog.isString()goog.isArray()などいくつかのClosure Libraryの関数にCompiler側が組み込みで対応することで、ユーザー定義はできないけどよくありそうなtype guardは実現できている。

必要性はわかるが、ここまでくるとちょっと複雑すぎるか。

#513 Meta-issue: this disambiguator

thisのバインドに関するmeta-issueでいくつかの種類の提案が入っている。

今のところ、TypeScriptはthisを型付けすることはできない。例えばDOMイベントリスナのthis。

elem.addEventListener('click', listener);
function listener(e: MouseEvent) {
  // ここでthisに型付けできないのでキャストが必要。
  (<HTMLElement>this).innerHTML = ...;
}

キャストが必要で不便という点と、引数に指定した関数の型検査ができないという両面で問題がある。

提案の一つとしてはこんな感じ。

// addEventListenerの型定義でのthis型指定
addEventListener(type: "click", listener: (this: HTMLElement, ev: MouseEvent) => any, useCapture?: boolean): void;

// listener側のthis型指定
function listener(this: HTMLElement, e: MouseEvent) {
  // キャスト不要
  this.innerHTML = ...;
}

// こういう関数は指定できない。
function invalid_listener(this: Date, e: MouseEvent) {
}

Closure Compilerでも同じように書くので個人的には違和感ない。これも既存JSコードやDOMに対応するためには必要なのでぜひいれてほしい。

#212 bind(), call(), and apply() are untyped

これもthisに関連するのだけど、Function.prototype.bind, call, applyの型付けが現状単純に効いてない。順序としてはおそらく前述のthis型付けが定まってからということになるだろう。

昔のissueに自分が投げたときは、良いんだけど実装大変だ的なこと言われたのだけど、ぜひよろしくね。

#185 Suggestion: non-nullable type

TypeScriptは現在プリミティブ型を含めてすべてがnullableである。

var a: !stringでnon-nullableな型を定義できる。逆に関数引数で

function f(a: !Array<string>): string {
  return a.join(', ');
}

と書いたらヌルポが起きないことをコンパイラが保証してくれる。

Closureでも同じ!を使う型定義だが、プリミティブ型はデフォルトでnon-nullableなのが大きく違う。Flowも型推論でできるだけnon-nullableになる。

TypeScriptはあとづけになるので、コンパイルオプションでベースをnon-nullableにするとか決められると良さそう。

これも必須でお願いします。

#1295 Suggestion: Type Property type

Backboneスタイルのfoo.get('bar')に型付けしようという試み。 やりたい気持ちはわかるが、個人的にはminifyとの相性が悪いのであまり使わないかな。

#1265 Comparison with Facebook Flow Type System

Facebook Flowとの型システムの比較。 おもしろいので読み物として読んでみましょう。


後半はぐだった上に日付を超えてしまったけど、オチもなく終わります。

切実度としてはthisとnon-nullableを早めにお願いします。