Node.jsではBabelを使ってトランスパイルをおこなって、どのWebブラウザでも動作するようにしたり、まだ仕様化されていないJavaScriptの機能を使ったりすることがあります。
Babelによってトランスパイルされる前のファイルに存在するクラスとトランスパイルされた後のファイルに存在するクラスをinstanceof
で比較するとfalse
が返されます。
トランスパイルしただけなので、同じ型のクラスのはずなのになぜ別のクラスと判定されるんだと疑問に思ったり、テストケースを書いているときに困ったりしました。
しかし、考えてみれば当たり前の話で、トランスパイルをおこなったことで別々のJavaScriptファイルになっていることから同じクラスではない、と判定されているのでしょう。
何が起きたのかと推測を先に書きましたが、ちょっとわかりにくいと思います。 どういうことなのかわかるようなコードを書いたので、一部を抜き出してながら説明します。 全てのコードはGitHubに置きましたのでご参考に。
環境
- Node.js: v16.20.0
- @babel/cli: v7.22.5
- @babel/core: v7.22.5
トランスパイル前後のファイル
まず、Babelのトランスパイル対象となるクラスを見ていきます。
トランスパイル前のファイル(src/user.js
)
class User { constructor(name) { this.name = name; } } module.exports = User;
トランスパイル後のファイル(dist/user.js
)
class User { constructor(name) { this.name = name; } } module.exports = User;
今回Babelの設定をおこなっていないため、ほぼそのままのコード(インデントの空白の数の変更と空行の削除)がトランスパイルされて出力されてきました。
また、モジュールの直下にindex.js
を用意しており、ここではトランスパイル後のファイルを読み込んでいます。
module.exports.User = require('./dist/user');
この後は、トランスパイル前のクラス(src/user.js
)とトランスパイル後のクラス(index.js
)を使っていきます。
トランスパイル前後のクラスをinstanceofで比較するとfalseになる
本題です。この段落では、test/user.test.js
ファイルを一部抜き出して説明します。
const BeforeUser = require('../src/user'); const AfterUser = require('../index').User;
トランスパイル前のUserクラスはBeforeUser
としてインポートし、トランスパイル後のUserクラスはAfterUser
としてインポートして使っています。
前 instanceof 前 および 後 instanceof 後
まず、BeforeUser
から生成したオブジェクトをBeforeUser
でinstanceofしてみました。
結果は当然trueになります。
it('User object from src/user.js is instanceof src/user.js', () => { const meg = new BeforeUser('meg'); expect(meg instanceof BeforeUser).to.be.true; // Passing! });
AfterUser
から生成したオブジェクトをAfterUser
でinstanceofしても同じく結果はtrueです。
前 instanceof 後
次に、BeforeUser
から生成したオブジェクトをAfterUser
でinstanceofしてみます。
結果はfalseになります。
it('User object from src/user.js is not instanceof index.js(dist/user.js)', () => { const rebecca = new BeforeUser('rebecca'); expect(rebecca instanceof AfterUser).to.be.false; // Passing! });
後 instanceof 前
さらに、前の段落の逆となるAfterUser
から生成したオブジェクトをBeforeUser
でinstanceofしてみます。
結果はfalseです。
it('User object from index.js(dist/user.js) is not instanceof src/user.js', () => { const jonah = new AfterUser('jonah'); expect(jonah instanceof BeforeUser).to.be.false; // Passing! });
推測:トランスパイル前後で別ファイル=別クラス?
ここからは推測です。
ここまでのコードでわかったように、トランスパイル前後のクラスは異なるクラスとして認識されています。 Babelによるトランスパイルは、元となるファイルから別のファイルを出力します。JavaScriptでは型情報が別にあるわけではないため、別ファイル=別クラスと認識された結果、今回のような現象が生じたと考えています。
TypeScriptであれば、型ファイル(*.d.ts
)によってこのような現象は起きないのではないかと(要検証)。
テストで困るケース
前述のような、同じファイル内でトランスパイル前のファイルとトランスパイル後のファイルの両方をインポートすることは実際にはほとんどありません。 ここでは、現実でテストをおこなったさいに困るケースを紹介します。
テスト対象のコードは以下です(src/hello.js
)。
トランスパイル後のファイルを読み込んでいます。関数の処理としては、与えられた引数のクラスがトランスパイル後のクラスであれば、「Hello」と挨拶を返し、そうでなければ「Userじゃないよ」と返します。
const User = require('../index').User; module.exports = function (user) { if (user instanceof User) { return `Hello, ${user.name}.`; } else { return `You are not User.`; } }
テストコードを見てみます(test/hello.test.js
)。
前段落と同じく、トランスパイル前のUserクラスはBeforeUser
として、トランスパイル後のUserクラスはAfterUser
としてインポートしています。
const BeforeUser = require('../src/user'); const AfterUser = require('../index').User;
it('User object from index.js(dist/user.js) is BeforeUser', () => { const kate = new AfterUser('kate'); const greeting = hello(kate); expect(greeting).to.equal('Hello, kate.'); // Passing! }); it('User object from src/user.js is not BeforeUser', () => { const david = new BeforeUser('david'); const greeting = hello(david); expect(greeting).to.equal('You are not User.'); // Passing! });
1つ目のテストケースではトランスパイル後のクラスであるため、ソースコードと同じクラスであり、挨拶が返ってきます。 それに対して、2つ目のテストケースではトランスパイル前のクラスであり、クラスが一致しないことから「Userじゃない」と言われています。
テストコードでは、トランスパイル前のファイルを読み込むことが多いと思っています。なので、2つ目のテストケースのようになり、テストが通らない状態が発生することがあります。
解消方法としては、ソースコードでもトランスパイル前のクラスを読み込む(通常であればこうする?)、テストコードでトランスパイル後のクラスを読み込むのどちらかをおこないましょう。
おわりに
Babelによるトランスパイル前後のクラスをソースコードとテストコードで混合して読み込むと、同じクラスのつもりなのにinstanceofがfalseになる、と困ることになります。
Babelのようなトランスパイルをおこなうモジュールを使って、instanceofがうまくいかなかった場合にはこのあたりを見直してみてください。