エフアンダーバー

個人開発の記録

BabelによるTypeScriptの変換でハマる

クトゥルフTRPG ツールにてGooglebotのためにBabelを入れたので、 ついでにTypeScriptの変換もBabelに任せてしまおうと作業していたところ、 不可解な現象に悩まされたためメモ。

不可解な警告

Dexie.jsというIndexed DBのラッパーライブラリを用いて次のようなコードを書いていました。

import Dexie from 'dexie';

export class StatusCacheDatabase extends Dexie {
    public readonly entries!: Dexie.Table<CacheEntry, number>;
    
    public constructor() {
        super("status-cache");

        this.version(1).stores({
            entries: "key, date",
        })
    }
}

このように書いておくとDexieが自動的にentriesプロパティに値を入れてくれて、

const db = new StatusCacheDatabase();

db.transaction("rw", db.entries, () => { ... }

と書けるはずだったのですが、これをBabelによる変換に変えたところ、

TypeError: Invalid table argument to Dexie.transaction(). Only Table or String are allowed

という警告が出るようになりました。

どうもentriesプロパティの値がundefinedになっているようでした。

原因

何かBabelの設定を間違えたのかといろいろ試したのですがうまくいかず、仕方なくDexieのコードを見ていくことに。

stores内のentriesを初期化するコードを追っていくと、 どうやら表と同名のプロパティが存在しない場合のみプロパティを定義するようで、 その部分のコードはだいたい次のような感じでした。

 if (!(tableName in obj)) {
    obj[tableName] = new Table(...);
}

なんとなく嫌な予感がしながらデバッガでobjのプロパティ一覧を見てみると、案の定undefinedという値のentriesプロパティが……。

ここまでくればコンストラクタで初期化されたであろうことは推測できるので、 Babelのリポジトリで原因を検索してみるとやはりありました

@babel/plugin-proposal-class-propertiesは初期値がない場合にクラスのプロパティをundefinedで初期化してしまうそうです。

対策

要は@babel/plugin-proposal-class-propertiesに仕事をさせなければいいので、 クラスのプロパティとしてではなくインターフェースとして書いて、型のマージを利用します。

import Dexie from 'dexie';

export class StatusCacheDatabase extends Dexie {
    public constructor() {
        super("status-cache");

        this.version(1).stores({
            entries: "key, date",
        })
    }
}

export interface StatusCacheDatabase {
    readonly entries: Dexie.Table<CacheEntry, number>;
}

もちろんBabelの設定でどうにかできればそれが一番いいのですが、今のところ方法はなさそうです。

バグか?仕様か?

TypeScriptを書いている身からすればバグっぽいですが、おそらく仕様じゃないかと思います。

@babel/plugin-proposal-class-propertiesはTC39のProposalに基づいたものだと思われるため、 その対象はTypeScriptではなくJavaScriptです。 JavaScriptでは型を書く必要がないため、プロパティの記述=定義であり、定義したからには何か初期値を入れるのが自然です。 宣言のためにプロパティを記述するというのはTypeScript固有の事情なのです。 そう考えるとむしろ、@babel/plugin-transform-typescriptが不要コードとして除去できていないバグなのかもしれません (Babelの仕組みをよく知らないのでそれが実現可能かまでは知りませんが)。

おわりに

TypeScriptの仕様とBabelによる変換の仕様が微妙に違うという話でした。 まあ、これについてはそのうち何らかの対策がとられると思うですが、 すでに使えないことが言及されている仕様も制限としてなかなかきついのですよね。 特に名前空間はIIFEの代わりとして結構便利だったので対応してほしいところです。 確かに機能的にはモジュールに置き換えることは可能でしょうが、 なんでもかんでもモジュールにすると面倒で仕方がないので。

……置き換えたばかりだけどなんだか元に戻したくなってきた。



執筆時のバージョン
@babel/plugin-proposal-class-properties: 7.3.0
@babel/preset-typescript: 7.1.0