2016-03-14 power-assert & typescript & babel とたたかった
2016-03-10 に power-assert の bug fix へ協力したことを書く。
TL;DR power-assert & typescript (target/module: es2015) & babel という構成。型定義に従って import * as assert from 'power-assert';
としたとき、実行時に Error を投げる問題があった。TypeScript / Babel での ES6 modules の扱いの違いによるもの。 power-assert 0.13.0 で対応された。
きっかけはこの tweet への reply 。
これの関連で https://t.co/rIGJ0wgmBP ES6 module 使わなくなったのか。 → TypeScriptでpower-assertを使う時の注意点 by @wadahiro on @Qiita https://t.co/fQQolNNpP8
— bouzuya (@bouzuya) March 7, 2016
@bouzuya TS の型定義周り、ようやく問題が収束したかと思ったのですが、まだ何かありますでしょうか……?
— Takuto Wada (@t_wada) March 8, 2016
問題は次のとおり。
@t_wada tsc -> babel と食わせると import * as assert from 'power-assert'; だと動かない (「 default がない」というエラーになる) んですよね……。
— bouzuya (@bouzuya) March 8, 2016
改めてはじめから書く。
power-assert の TypeScript 向けの型定義 (.d.ts) の export は次のようになっていた。
export default assert;
この状態であれば import ... from ...;
が可能になり、 import * as ... from ...;
が不可になる。つまり次のようになる。
import assert from 'power-assert'; // export default では OK
import * as assert from 'power-assert'; // export default では NG
この型定義が変更された。変更の理由は TypeScriptでpower-assertを使う時の注意点 - Qiita に書かれているとおりだ。TypeScript の出力が power-assert の動作しない形式になってしまう問題を回避するためだ。
power-assert の .d.ts の export は次のように変更された。
export = assert;
この状態であれば、書きかたは逆になる。import ... from ...;
が不可になり、 import * as ... from ...;
が可能になる。つまり次のようになる。
import assert from 'power-assert'; // export = では NG
import * as assert from 'power-assert'; // export = では OK
TypeScript がこの記述をどう解釈するのか。それは次のとおりだ。
import foo from 'foo'; // export default foo; // module.exports.default = foo;
import * as foo from 'foo'; // export = foo; // module.exports = foo;
TypeScript & Babel 構成では module: es2015, target: es2015 を使う。結果として import * as assert from 'power-assert';
な .js が出力される。
次に Babel 側に移る。
Babel に上記のふたつをそれぞれ食わせて解釈を見てみる (https://babeljs.io/repl/ で試せる) 。
// import assert from 'power-assert';
// assert(1 === 1);
'use strict';
var _powerAssert = require('power-assert');
var _powerAssert2 = _interopRequireDefault(_powerAssert);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
(0, _powerAssert2.default)(1 === 1);
// import * as assert from 'power-assert';
// assert(1 === 1);
'use strict';
var _powerAssert = require('power-assert');
var assert = _interopRequireWildcard(_powerAssert);
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; } }
assert(1 === 1);
import ... from ...;
は __esModule
が truthy なら default に既に function があるものとし、なければ強制的に { default: obj }
の形で wrap する。
import * as ... from ...;
は __esModule
が truthy なら そのまま、そうでなければ新しい newObj
にすべてを詰め直して default
に obj
を設定する形で wrap する。
……と書いてもピンと来ないので power-assert を例に挙げる。
前者は Error を投げないが、上記の Qiita 記事にある通り assert(...)
の形を維持できないので power-assert が動作しない。
後者は今回の修正まで Error を投げていた。
TypeScript の出力に該当する後者の挙動をもうすこし追う。今回の修正前の power-assert は __esModule
が falsy で module.exports = assert;
していた。この状態で import * as assert from 'power-assert';
すると次のようになる。
var _powerAssert = require('power-assert'); // _powerAssert is function
var assert = _interopRequireWildcard(_powerAssert); // assert is { default: [Function] }
// ...
assert(1 === 1); // assert is NOT function
なるほど Error を投げる。
回避策として import assert = require('power-assert')
したくなる。しかし TypeScript で module: es2015, target: es2015 の場合はこれが Error になる。つまり今回の修正までは TypeScript で es2015 を出力し、 Babel で es5 を出力する構成で power-assert は使えなかった、と。
今回の修正でどうなったか。
power-assert 側で __esModule
を truthy にする変更が入った。module.exports.default
にも値が入っているので、これで Babel が ES6 modules だと見なして wrap をやめてくれる。import * as assert from 'power-assert';
としておけば TypeScript + Babel 構成でも power-assert を使える。やったね。
今回の問題の原因は何か。それは TypeScript と Babel で ES6 modules の扱いに違いがあることだ。export default foo / import foo / import * as foo を module.exports = function() {};
に対してどう適用するかの違いによるものだ。
せっかくなので、もうすこし分かりやすい単位で Qiita にも書くつもり。
ちなみに再現用の repository は bouzuya/typescript-power-assert-babel-kanashimi 。
追記: Babel と TypeScript の ES6 modules の import の解釈の違い - Qiita