TypeScriptに移行して気づいた10の事実

ついに Vim Vixen をTypeScript に移行しました。 今まで強がりで ECMAScript で書いてたのですが、静的型付き言語の便利さに負けてついに移行しました。 その時に新しい発見がいくつかあったので簡単にまとめます。

エコシステムが十分に育っている

TypeScript に移行するのなら、トランスパイラ本体だけではなく周辺ライブラリのサポートも必要です。 たとえば Vim Vixen では Webpack でのビルドや Linter のチェックをしてます。 また使ってるライブラリの型定義もほしいです。 それら TypeScript 以外とのエコシステムの成熟度が、今回の移行の鍵でした。

結論として無事移行できました。 Vim Vixen のライブラリ利用状況は以下のようになりました。

  • ビルド: Webpack + ts-loader
  • Linter: ESLint + @typescript-eslint/eslint-plugin
  • テスト: karma-webpack + mocha/chai

若干@typescript-eslint/eslint-plugin で不具合がありますが、現在絶賛開発中なので将来にも期待できます。

型アノテーションをつけるだけで移行完了、ではない

TypeScript は ECMAScript(ECMAScript 2015)互換の言語なので、当初はとりあえず型アノテーションを付けて終わらせようと思ってました。 しかし実際は気付かなかった記述ミスや余分なパラメータがありました。 TypeScript の型チェックがパスするまで、それらを削除したりundefinedチェックを追加しました。

予想してたよりも TypeScript 移行に時間がかかりました。 結局、型アノテーションをつけるだけでは終わらなかったのですが、未然に多くのバグを防ぐことができたので良しとします。

Optional なプロパティに気づける

WebExtensions API の型定義にweb-ext-typesを使いました。 型定義を使うと、API が Optional (undefinedになりうる) プロパティも事前に検知できてバグを未然に発見できます。

例えば以下のコードは、現在のタブにメッセージを送るコードです。 このコードはトランスパイル時にエラーが出ます。

let tabs = await browser.tabs.get({ active: true })
browser.tabs.sendMessage(tabs[0].id, {
  type: "greeting",
  text: "Hello, world!",
})

なぜならTab.id フィールドは Optional だからです。 browser.tabs.sendMessage() の第 1 引数はnumberなので、Optional なフィールドは渡せません。

index.ts:3:26 - error TS2345: Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
  Type 'undefined' is not assignable to type 'number'.

3 browser.tabs.sendMessage(tabs[0].id, {
                           ~~~~~~~~~~

解決するにはundefinedチェックするか、

let tabs = await browser.tabs.query({ active: true })
let id = tabs[0].id
if (typeof id === "undefined") {
  return
}
browser.tabs.sendMessage(id, {
  type: "greeting",
  text: "Hello, world!",
})

undefinedではないといい切れるなら!!を付けます。

let tabs = await browser.tabs.query({ active: true })
browser.tabs.sendMessage(tabs[0].id!!, {
  type: "greeting",
  text: "Hello, world!",
})

仕様変更する場合がある

先程 TypeScript は Optional な型に気づけて良いと書きました。 しかし何でもかんでも無条件にundefinedチェックを記述できるとは限りません。 例えば先程のTab.idですが、それを別のメソッドに渡すとします。

// 移行前
processTab(tabs[0].id)
// 移行後、undefinedチェックを追加。
let id = tabs[0].id
if (typeof id === "undefined") {
  return
}
processTab(id)

一見 TypeScript の方が堅牢に見えて良いコードに思えます。 しかし前者と後者は振る舞いが異なります。

移行前のコードに不具合があるかも知れませんが、もしかすると呼び出し側が外発生を期待してるかも知れません。 どちらにせよ、むやみにundefinedチェックを挿入すると、意図しない仕様変更をする可能性があるので気をつける必要ああります。


まだまだあった気がするけど収まりが悪いのでこのへんで。 思い出したらまた書きます....


Profile picture

Shin'ya Ueoka

B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。