esbuildでビルドして配信されてるパッケージがinstanceofできない問題の対応
ある日「別パッケージから読み込んだ同一のパッケージのクラスをinstanceofすると常にfalseになる」という問題がどうにかならないかと相談されて、自分はハマった事がなかったのでそんな事があるのかと気になったので調べた
もしより良い方法があれば教えて欲しい。
問題概要
PackageCからPackageAとPackageBを読み込んでいて、PackageAのクラスをPackageBでinstanceofできない
Mermaidで書くとこんな感じ
また各パッケージではesbuildでビルドしていて、PackageAとPackageBはesbuildでビルドされたパッケージを配信している
調べてみると同じような問題を抱えている人がいた
- Babelによるトランスパイル前後のクラスをinstanceofで比較するとfalseになる - s1r-Jの技術ブログ
- javascript - Why does the
instanceof
operator return false on instance passed to library? (No inheritance involved) - Stack Overflow
原因
esbuildで以下のコードをesbuild --bundle --platform=node --format=esm --outfile=out.js index.ts
でビルドすると以下のようになる
元のコード
export class Hoge {
private x: number
constructor(x: number = 1) {
this.x = x
}
getValue(): number {
return this.x
}
}
esbuild実行後
var Hoge = class {
x
constructor(x = 1) {
this.x = x
}
getValue() {
return this.x
}
}
export { Hoge }
このような結果になる場合に、PackageAとBでそれぞれesbuildでビルドすると複数のHogeクラスが定義されてしまうのでCで以下のような処理を書いた時に、B内部のinstanceofによるを比較は常にfalse
になる
// package-bのコード
import { Hoge } from 'package-a';
// テスト用に値を返す関数
export const getHogeValue = (v: unknown): number {
return v instanceof Hoge ? v.getValue() : -1;
}
// package-cのコード
import { Hoge } from 'package-a';
import {getHogeValue} from 'package-b';
const hoge = new Hoge(5);
console.log(getHogeValue(hoge)); // -1になる。理想は5が返ってきて欲しい
解決方法
解決方法としてはvar Hoge = class
の部分を共通化して同じものを見る必要がある
どこまでパッケージに手を入れれるのかで変わってくるが自分が考えたのは以下の3つ
可能であれば3のそもそもライブラリ側でtranspileをしないようにするのが一番良い気がする
1. esbuildのexternal
オプションを使う
PackageBのビルド時にesbuildのexternal
オプションを使うと、PackageAをバンドルしなくなる
# package-bのビルド
esbuild --bundle --platform=node --format=esm --outfile=out.js --external:package-a index.ts
ただそもそもPackageAのようなライブラリ側でバンドルしないようにする(--bundle
を無くす)のが一番良い気がする
2. PackageBからAをexportする
PackageB以外でAをimportしてinstanceof
したりしないという制約ができてしまうが、PackageBからAをexportすることで解決することもできる
// package-bのコード
import { Hoge } from 'package-a';
export * from 'package-a';
// テスト用に値を返す関数
export const getHogeValue = (v: unknown): number {
return v instanceof Hoge ? v.getValue() : -1;
}
3. ライブラリ側でtranspileしないようにする
そもそもesbuildでのビルドをやめtsc
でビルドしたものを使用するようにする(ファイルの指定・targetなどは省略)
tsc --outDir dist --declaration
これでほぼ型注釈が外れただけのコードが出てくるはずなので、B, Cで同じAを見ることができる
まとめ
今回はトランスパイルした結果をライブラリとして配信していた結果、同じクラスでも別のクラスとして扱われてしまい、instanceof
ができない問題が発生した。個人的にはアプリケーション側(例だとPackageC)でminifyなりトランスパイルするので、ライブラリ側では型注釈を外すだけで良いのではないかと思う
以下テストに使用したコード yuzu-sandbox/node-import-sandbox: import package test repository