概要
こんにちは。サーバーサイドエンジニアの窪田です。
ヤプリでは
- Webフロントエンド (Nuxt.js, Next.js)
- 自動化しているアプリのbuild処理 (Node.js)
- 一部のPush通知の送信基盤 (Node.js)
でJavaScriptとTypeScriptを使っていたり、試験的にDenoを使ったプロダクトもあったりして、 JS, TSへの理解が必要になってきます。
最近、TypeScriptのclassの継承周りで一部勘違いしていたことがあったので、 その事例を交えて整理してみます。
JavaScriptにおける継承の挙動
$~ node --version $~ v18.2.0
の環境で試します。
// tryExtends.js class BaseError extends Error {} class CustomError extends BaseError {} const hogeError = new CustomError(); console.log(hogeError instanceof CustomError); console.log(hogeError instanceof BaseError); console.log(hogeError instanceof Error);
$~ node tryExtends.js
この処理を実行した時、3個のconsole.logは全てtrueになります。
TypeScriptにおける継承の挙動
$~ npx tsc --version $~ Version 5.1.3 $~ node --version $~ v18.2.0
の環境で試します。
今度はTypeScriptでコンパイルしたのち実行してみます。
// tryExtends.js class BaseError extends Error {} class CustomError extends BaseError {} const hogeError = new CustomError(); console.log(hogeError instanceof CustomError); console.log(hogeError instanceof BaseError); console.log(hogeError instanceof Error);
$~ npx tsc tryExtends.ts && node tryExtends.js
この結果はどうなるでしょうか?
答えは、「tsconfigのtargetによる」です。
$~ npx tsc --lib dom, es2022 --target es2022 tryExtends.ts && node tryExtends.js
このようにtargetをES6以上に設定した場合の出力は
console.log(hogeError instanceof CustomError); // -> true console.log(hogeError instanceof BaseError); // -> true console.log(hogeError instanceof Error); // -> true
になります。 一方で
$~ npx tsc --lib dom, es2022 --target es5 tryExtends.ts && node tryExtends.js
のようにtargetをES5以下を指定した場合の出力は
console.log(hogeError instanceof CustomError); // -> false console.log(hogeError instanceof BaseError); // -> false console.log(hogeError instanceof Error); // -> true
になります。
なぜこの違いがあるのか
結局はESのversionの違いが本質的な違いです。 TypeScriptかどうかはあまり関係ありません。 ES6でclass構文が登場したため、targetがES6以降なら対応がありますが、ES5以前は自前で継承を実装する必要があります。
そもそも、JavaScriptにおいて、 class A がclass Bを継承しているということは、class Aのインスタンスから辿れるプロトタイプチェーン上にB.prototyeが登場するということです。
以下をES6をターゲットにコンパイルし、実行すると、
class BaseError extends Error {} class CustomError extends BaseError {} const hogeError = new CustomError(); console.log(hogeError instanceof CustomError); // -> true console.log(hogeError instanceof BaseError); // -> true console.log(hogeError instanceof Error); // -> true console.log(custom.__proto__ === CustomError.prototype); // -> true console.log(custom.__proto__.__proto__ === BaseError.prototype); // -> true console.log(custom.__proto__.__proto__.__proto__ === Error.prototype); // -> true
コメントで示した出力になります。 customオブジェクトのプロトタイプチェーンを辿ると、CustomError, BaseError, Errorオブジェクトのprototypeをそれぞれ参照していることを確認できます。
一方で、ES5をターゲットとしたときはCustomError, BaseErrorのprototypeがcustomオブジェクトのプロトタイプチェーンに現れません。 そのため、ES5をターゲットにする場合は明示的にprototype登録する必要があります。
class BaseError extends Error { constructor() { super(); Object.setPrototypeOf(this, new.target.prototype); // <- prototype登録 } } class CustomError extends BaseError {} const hogeError = new CustomError(); console.log(hogeError instanceof CustomError); // -> true console.log(hogeError instanceof BaseError); // -> true console.log(hogeError instanceof Error); // -> true console.log(custom.__proto__ === CustomError.prototype); // -> true console.log(custom.__proto__.__proto__ === BaseError.prototype); // -> true console.log(custom.__proto__.__proto__.__proto__ === Error.prototype); // -> true
なぜこれを考えたかの経緯
実は、現状でプロダクトコードにObject.setPrototypeOf()
が乱用されていた部分がありました。
ES5の時代をそんなに生きていない自分にはよくわかっておらず、なぜその記述が必要だったのかを理解できていませんでした。
しかも、tscコンパイルのtargetは既にES2018に上げられていたため、prototype登録は必要なくなっている状態で、1行を消しても動作する状態にあり余計に混乱したという経緯がありました。
教訓
- JavaScriptとTypeScriptでclass構文は違う(当たり前。。。)
- classだからと言ってコンパイル後のJSがES6以上と思い込んではいけない
- もし、ES5以下 -> ES6以上のtargetのアップデートをする場合は、ES5でしか動作しないコードは消す
これらのことは意識していきたいです。
まとめ
今回、ES5が関わる少しニッチな内容でしたが、今一度class構文とは何なのかを考えることができてよかったです。
冒頭で述べた通り、ヤプリではWebフロントを中心にJavaScript, TypeScriptのプロダクトがあります。 興味がある方はぜひカジュアル面談にお越しください。