OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

TypeScript コンパイルなしでこっそり型の恩恵を得る

TypeScript Advent Calendar 23日の記事になります。

TypeScript良さそうだなーと思うんだけど既存のビルドプロセスに組み込むのがめんどくさかったり、使いたいんだけどチーム開発で自分だけ使うわけにいかないというようなしがらみがあったりします。

TypeScript 2.3 から、JavaScriptのままでもコンパイラが静的な型チェックをしてくれるようになったんですけど、すごさのわりに自分の観測範囲ではあまり話題になっていない。
まあすでにTypeScript使ってる人にとってはふつう関係ない話なので、そりゃそうかという気もしますが。

ともかくこれなら簡単に導入できて、コンパイル(トランスパイル)を挟むこともなく、自分だけこっそり型チェックの恩恵を得るようなことができます。

環境

リアルタイムでコンパイルチェックしてくれるエディタがいいです。こだわりがなければVisual Studio Codeでいいでしょう。

TypeScriptをインストールしておく必要があります。

npmなどでローカルインストールしてもいいですし、チームで使うディレクトリを汚したくなければグローバルインストールでも大丈夫です。

バージョンは新しいほうがいいです。ここでの検証には 3.2.2を使用しています。

サンプルコード

ES5で簡単な疑似クラスを作ってみます。

"use strict";  
var Person = (function () {  

    function Person(name, age) {  
        this.name = name;  
        this.age = age;  
    }  
    Person.prototype.hello = function() {  
        return "Hello, I'm " + this.name;  
    }  
    Person.prototype.olderThan = function(age) {  
        return this.age > age;  
    }  
    return Person;  
}());  

使われ方。

var john = new Person("John", 20);  
var message = john.hello(); // "Hello, I'm John"  
var isOlderThan18 = john.olderThan(18); // true  

型チェックがないので、以下のようなコードを書いても、実行時までエラーに気づくことができません。

var john = new Person("John", "20");    // 引数の型が違う  
var message = john.helllo(); // タイプミス  
var isOlderThan18 = john.olderThan(); // 引数がない  

最低限のチェックは @ts-check を書くだけ

ここが意外と知られてない気がするんですけど、ソースファイルの先頭に @ts-check とコメントを入れるだけで、それなりにチェックしてくれます。

// @ts-check  

"use strict";  
var Person = (function () {  

    function Person(name, age) {  
        this.name = name;  
        this.age = age;  
    }  
    Person.prototype.hello = function() {  
        return "Hello, I'm " + this.name;  
    }  
    Person.prototype.olderThan = function(age) {  
        return this.age > age;  
    }  
    return Person;  
}());  

var john = new Person("John", "20");  
var message = john.helllo(); // <-ここがコンパイルエラー  
var isOlderThan18 = john.olderThan();  

john.helllo() のように、存在しないメンバの参照はエラーにしてくれます。

このほか john.hello("foo") のように、不要な引数を指定した場合もエラーになります。

これだけでも、何もないよりはだいぶ嬉しくないですか?

おい john.olderThan() (引数なし)はなんでエラーにならねんだよと思われるかもしれませんが、これは olderThan メソッドの引数 age の型がわからない(undefinedを許容するかもしれない)ためですね。

JSDocで型情報を追加する

JSDoc形式のコメントで型情報を記述します。

// @ts-check  

"use strict";  
var Person = (function () {  
    /**  
     * インスタンスを生成します。  
     * @param {string} name   
     * @param {number} age   
     */  
    function Person(name, age) {  
        this.name = name;  
        this.age = age;  
    }  
    /**  
     * 挨拶の言葉を返します。  
     * @returns {string}  
     */  
    Person.prototype.hello = function() {  
        return "Hello, I'm " + this.name;  
    }  
    /**  
     * この人が指定した年齢より上かどうかを返します。  
     * @param {number} age  
     * @returns {boolean}  
     */  
    Person.prototype.olderThan = function(age) {  
        return this.age > age;  
    }  
    return Person;  
}());  

var john = new Person("John", "20");  // <-コンパイルエラー  
var message = john.helllo();          // <-コンパイルエラー  
var isOlderThan18 = john.olderThan(); // <-コンパイルエラー  

日本語も書きましたが、型チェックのために必要なのはアノテーションの部分だけです。
これで、引数や戻り値について型の不一致を検出してくれるようになります。

tsconfig.jsonで型チェックをデフォルト有効に

@ts-check なんて書いてたら、なんだこの謎コメントは!消せ!と言われる地獄もあるかもしれません。
そうでなくても、毎回書くのがめんどくさい場合は、tsconfig.jsonをルートディレクトリに作って、ここでデフォルトチェックONにしておくことができます。
ついでに、より厳格なチェックのために "strict": true を入れておくのもいいでしょう。

{  
  "compilerOptions": {  
    "allowJs": true,  
    "checkJs": true,  
    "strict": true  
  }  
}  

こうすると、現在のtscバージョンでは、以下の this がエラーになると思います。

function Person(name, age) {  
    // 'this' implicitly has type 'any' because it does not have a type annotation  
    this.name = name;  
    this.age = age;  
}  

暗黙的なanyゆるさんぞというやつですね。

この場合は、このコンストラクタのJSDocに @constructor を追加することで解消されます。
あるいは tsconfig.jsonのコンパイラオプションに "noImplicitThis": false を追加することでこのチェック自体を無効にすることもできます。

このほかにもJSDocで定義できることがいろいろあります。詳しくはこちらをご覧ください。

以上!