TypeScript + babelでハマった話


# 概要
TypeScript(というかES6)のimport文を、いまいち書き方を理解しないまま下記の記事を参考に書いていました。

[TypeScript 1.5.3 変更点](http://qiita.com/vvakame/items/9b9fde6c71aae6a824c0)
> なので、とりあえず * as foo の形式でfooにモジュールを束縛して使うのが今までの使い方とあまり差がなく、わかりやすいでしょう。

という訳で基本的には`import * as foo from 'foo'`の形式でimport文を書いていたのですが、これが`target --es5`のケースと、`target --es6` + babelのケースで若干違いがありハマったのでメモ。ちなみに上の記事が問題というわけではなく、よく理解せずに使っていた私が悪いという話です。

## そもそも何故`target --es6` + babelと2度変換をかけるのか
`target --es6`で変換したコードを、さらにバベってES5にするというのは一見すると手間が増えただけのように思えます。手間が増えるのを承知のうえでこの構成にしている理由は、今後のメンテナンス性にあります。

TypeScriptは広く普及しているといえどAltJSの１つであるという事実に変わりはありません。一方ES6は、今後スタンダードになることが確実です。なので今後コードを引き継ぐことを考えると、ES6に寄せていきたい訳です。ただ静的型付けであるTypeScriptは個人的にはどうしても使いたいので、一度`target --es6`での出力を挟むことで **いつでもTypeScriptを切り捨てられる環境にしつつ、TypeScriptを使っていく** と良いのかなと思います。

# 例

## 実行環境
node v0.12.7
tcs 1.6.2
babel 5.8.23


## サンプル
元のTypeScriptコードです。importでlodashを読み込み、シンプルにそれを使っているだけですね。

```ts:元のTypeScriptファイル
/// <reference path="./typings/bundle.d.ts" />
import * as _ from 'lodash';
const array = [1, 2, 3];
_.forEach(array, (n) => { console.log(n); });
```

### target --es5のケース
tscで`target --es5`に指定した場合、次のように変換されました。import文がcommonJSのrequireにシンプルに変換されています。今まで`require`で書いていたときと全く同じ形に変換され、違和感なく使うことが出来ます。

```js:es5に変換されたコード
/// <reference path="./typings/bundle.d.ts" />
var _ = require('lodash');
var array = [1, 2, 3];
_.forEach(array, function (n) { console.log(n); });
```

### target --es6 => babelのケース
ハマったのがこのケース。TypeScriptを一旦`target --es6`の形式で出力し、それをBabelにかけて最終的なES5コードにしています。

ES6への変換後のコードがコチラ。今回は型を特に使ってなかったので、何も変わらないです。

```js:ES6に変換されたコード
/// <reference path="./typings/bundle.d.ts" />
import * as _ from 'lodash';
const array = [1, 2, 3];
_.forEach(array, (n) => { console.log(n); });
```

そして以下がes6のコードをbabelにかけた例。`require`に対して、`_interopRequireWildcard`という内部関数が１枚挟まっています。

```js:babel変換後
/// <reference path="./typings/bundle.d.ts" />
'use strict';

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } }

var _lodash = require('lodash');

var _ = _interopRequireWildcard(_lodash);

var array = [1, 2, 3];
_.forEach(array, function (n) {
  console.log(n);
});
```

分かりづらいので展開すると以下のような形に。

```js:babel変換後
function _interopRequireWildcard(obj) {
	if (obj && obj.__esModule) {
		return obj;
	}
	else {
		var newObj = {};
		if (obj != null) {
			for (var key in obj) {
				if (Object.prototype.hasOwnProperty.call(obj, key))
					newObj[key] = obj[key];
			}
		}
		newObj['default'] = obj;
		return newObj;
	}
}
```

ここに見事に引っかかりました。`target --es5`の変換と、`target --es5` => babelの変換は等価ではなく、後者の場合のみ`hasOwnProperty`でないプロパティは削除されて読み込まれるようです。

## どんな問題が起きるのか？

`target --es5`のコードを、`target --es6` + babel に置き換えてもコードは書きかえなくていいだろう、と思っていると痛い目見ます。

私がハマったのはnode.jsで広く使われる認証モジュール`passport.js`を使っていたときで、ざっくり書くと以下のようなコードです。これを`target --es6` + babelの構成にした途端、`use`プロパティが無いと怒られてしまいました...
`passport.js`のコードを見てもらえるとわかりますが、`use`及び他のメソッドはprorotypeとして定義されており、`require()`したときbabelが吐いたコードによって除外されてしまっています。

```js:問題が起きる例
/// <reference path="./typings/bundle.d.ts" />

import * as passport from 'passport';
import {Strategy as FacebookStrategy} from 'passport-local';

passport.use(new FacebookStrategy(() => {
  //
}
```

解決方法としては、以下のようにワイルドカード指定をやめることが考えられます。

```js:解決策
/// <reference path="./typings/bundle.d.ts" />

import Passport from 'passport';
import {Strategy as FacebookStrategy} from 'passport-local';

passport.use(new FacebookStrategy(() => {
  //
}
```

すると以下のように変換されるはずです。こちらのケースなら`hasOwnProperty()`のチェックが走らず、今まで通りの感覚で`require()`することが出来ます。

```js:解決策のbabel後
/// <reference path="./typings/bundle.d.ts" />
'use strict';

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

var _passport = require('passport');
var _passport2 = _interopRequireDefault(_passport);
```

ただし、問題があり2015/10/28時点で **DefinitelyTypedのpassport.js型定義ファイルがこの形式のimportに対応していないません**。コンパイルで怒られてしまう。
DefinitelyTypedにプルリクを送ればいいのかもしれませんが、今後も他のモジュールで同じ事を繰り返すんだろうなと思ってやる気がでない..

# まとめ
`target --es5`と`target --es6` + babelは等価ではありません。注意しましょう。

自分で書いてて「本当かコレ」感があったので、詳しい人のご意見お待ちしております。

他にもPromise(bluebird)使いたい時に、`import * Promise as 'bluebird'`とすると`target --es6`だと「ES6にはPromise既に定義されていますよ」と怒られる問題もどう解決したらいいんだろうか。node 0.10系でバベったES6コードを動かしたいときに困る。

# 参考
Babelで理解するEcmaScript6の import / export
http://qiita.com/inuscript/items/41168a50904242005271

TypeScript 1.5.3 変更点
http://qiita.com/vvakame/items/9b9fde6c71aae6a824c0