Babelによるトランスパイル前後のクラスをinstanceofで比較するとfalseになる

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がうまくいかなかった場合にはこのあたりを見直してみてください。