🔫

target: es2022 にして、useDefineForClassFieldsに足を撃ち抜かれそうになった

2024/01/20に公開

対象読者

TypeScriptは、LegacyDecoratorとEnumとParameterPropertiesを抑えておけば、その他には変換後の挙動に変化はないでしょ。target上げてもビルド通っちゃえば楽勝でしょ。と思い込んでいた過去の自分。

なんの話をしてるんです?

TypeScriptのConfigのKeyには、useDefineForClassFieldsがある。これは、targetがES2022以上(ESNextを含む)の場合は、Defaultがtrueになり、それ以外の場合は、falseになる。

つまりどういうことです?

次のようなコードを考える。

// main.ts
export class Foo {
    p1: string | undefined
    constructor(p1: string | undefined = undefined) {
        if (p1) {
            this.p1 = p1;
        }
    }
}

const foo = new Foo();
console.log(Object.keys(foo));

target: es2016 でトランスパイルして実行する。

 npx tsc --target es2016 --outDir dist_es2016
 node dist_es2016/main.js
 []

fooのpropertyのkeyは空配列になっている。

target: es2022 でトランスパイルして実行する。

npx tsc --target es2022 --outDir dist_es2022
node dist_es2022/main.js 
[ 'p1' ]

keyにp1が生えてる?????

outputのdiff を見てみよう。

difft dist_es2016/main.js dist_es2022/main.js 
main.js --- JavaScript
2 2 Object.defineProperty(exports, "__esModule", { value: true });
3 3 exports.Foo = void 0;
4 4 class Foo {
. 5     p1;
5 6     constructor(p1 = undefined) {
6 7         if (p1) {
7 8             this.p1 = p1;

es2022の方は、p1が宣言されていることがわかります。なぜ、そうなるのかについては、ここら辺を読む必要がありそう。ありそうなんだが、、、ありそう。

で、実際に何が起きて困ったんです?

テストデータの作成用に以下のようなコードがあり、挙動が変わってしまいました。そのためテストが失敗するようになりました。その原因を探るのに苦労しました。

// testData.ts
import { Foo } from './main';

export function overrideFoo(origin: Foo, props: Partial<Foo>): Foo {
    return {
        ...props,
        ...origin
    }
}

const foo = new Foo();
const foo2 = overRideFoo(foo, { p1: 'foo' });
console.log(Object.entries(foo2));

es2022以前は、"foo"がセットされていたのに、es2022にしたときundefinedになってしまった。

node dist_es2016/testData.js
[ [ 'p1', 'foo' ] ]
node dist_es2022/testData.js
[ [ 'p1', undefined ] ]

どう直したんです?

今回の例だとoverrideのコードが、そもそも名前と挙動があってなかったのでそこを修正しました。

export function overrideFoo(origin: Foo, props: Partial<Foo>): Foo {
    return {
        ...origin,
        ...props,
    }
}

そもそもこれが起きないようにするにはどうすればよかったんだろうか?

いつか書く。

CureApp テックブログ

Discussion