15 min/d

ぼうずやのにっき

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 。

問題は次のとおり。

改めてはじめから書く。

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 にすべてを詰め直して defaultobj を設定する形で 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