TypeScriptのenumを使わないほうがいい理由を、Tree-shakingの観点で紹介します

こんにちは。LINE Growth Technology UITチームの慶島(@pittanko_pta)です。
この記事では、TypeScriptのenumを使わないほうがいい理由を、Tree-shakingの観点で紹介します。

検証環境

  • TypeScriptからJavaScriptへのトランスパイルは https://www.typescriptlang.org/play (TypeScript 3.9.2 / targetはESNext) で行いました。
  • Tree-shaking の挙動については https://rollupjs.org/repl/ にトランスパイルしたJavaScriptコードを貼り付けて検証しました。
  • Babelを使用したトランスパイルを検証する際には https://babeljs.io/repl (Babel 7.10.3) で行いました。

Tree-shakingとは?

本題に入る前に、まずはTree-shakingについて説明します。
Tree-shakingとはひとことで言うと、使われていないコードを削除する機能のことです。

Tree-shakingにより、exportしているのにどこからもimportしていないモジュールや、デッドコードなどを削除することができ、バンドルのサイズを減らすことができます。
バンドルのサイズが減ることで、ユーザーがページを表示する時間を短縮できるメリットがあります。

不要なコードが削除されることを、木を揺すって葉っぱなどが落ちる様子になぞらえて、Tree-shakingと呼ばれています。

enumについて説明

enumは列挙型と呼ばれ、定数をひとまとめにしておくのに便利な機能です。
任意の数字や文字列を割り当てることもできます。
また、定義したenumは型として使用することも可能なので、バグの少ないコードを書くのに貢献します。

 
// 何も指定しない場合は0から数字が振られていきますが…
enum MOBILE_OS {
  IOS, // 0
  ANDROID // 1
}

// 任意の数字や文字列を割り当てることもできます
enum MOBILE_OS {
  IOS = 'iOS',
  ANDROID = 'Android'
}

// 以下のように、型として扱うことも可能です
const os: MOBILE_OS = MOBILE_OS.IOS
function detectOSType(userAgent: string): MOBILE_OS {
    // 略
}

また、enumはJavaScriptにはなく、TypeScriptが独自に実装している機能です。
便利な反面、JavaScriptでは利用できないため、似たようなことをする場合、
以下のようなオブジェクトを使ったコードを使うことが多いと思います。

 
const MOBILE_OS = {
    IOS: 'iOS',
    ANDROID: 'Android'
}
console.log(MOBILE_OS.IOS) // iOS

本題: TypeScript + enum の Tree-shakingはどうなのか

前述のように便利なenum機能ですが、TypeScriptが独自に実装しているゆえの課題があります。 以下のようなTypeScriptコードを書いた場合、

 
export enum MOBILE_OS {
  IOS,
  ANDROID
}

// 文字列を割り当てた場合
export enum MOBILE_OS {
  IOS = 'iOS',
  ANDROID = 'Android'
}
 

以下のようなJavaScriptコードにトランスパイルされます。

 
export var MOBILE_OS;
(function (MOBILE_OS) {
    MOBILE_OS[MOBILE_OS["IOS"] = 0] = "IOS";
    MOBILE_OS[MOBILE_OS["ANDROID"] = 1] = "ANDROID";
})(MOBILE_OS || (MOBILE_OS = {}));

// 文字列を割り当てた場合
export var MOBILE_OS;
(function (MOBILE_OS) {
    MOBILE_OS["IOS"] = "iOS";
    MOBILE_OS["ANDROID"] = "Android";
})(MOBILE_OS || (MOBILE_OS = {}));

JavaScriptの言語仕様に存在しないものを実現するために、TypeScriptコンパイラはIIFE(即時実行関数)を含んだコードを生成します。

Rollupなどのバンドラーは、IIFEを「使っていないコード」と判断することができません。 MOBILE_OS をimportして、実際には使っていないとしても、最終的なバンドルに含まれてしまいます。(Tree-shakingができません)

では何を使えばいいのか?

Union Typesを使いましょう。(以後は文字列を割り当てたenumのみ紹介します) 以下のようにTypeScriptのコードを書くことで、

 
const MOBILE_OS = {
  IOS: 'iOS',
  Android: 'Android'
} as const;
type MOBILE_OS = typeof MOBILE_OS[keyof typeof MOBILE_OS]; // 'iOS' | 'Android'
 

以下のようなJavaScriptコードにトランスパイルされます。

 
const MOBILE_OS = {
    IOS: 'iOS',
    Android: 'Android'
};
 

TypeScriptのコード上では 定義した MOBILE_OS の型定義の恩恵を受けつつ、JavaScriptにトランスパイルしてもIIFEが生成されないのでTree-shaking可能になります。

今までJavaScriptのオブジェクトでenumっぽいことを表現していた場合、トランスパイルされるJavaScriptにほとんど差分を出すことなく、型の恩恵を受けられるようになるのも嬉しいポイントですね。

あれ、const enumは?

const enum は TypeScriptで書く場合に enum とほぼ同じに感じられますが、enumの中身がトランスパイル時にインライン展開される点が異なります。以下のTypeScriptコードは

 
const enum MOBILE_OS {
    IOS = 'iOS',
    ANDROID = 'Android',
}
const ios = MOBILE_OS.IOS

以下のJavaScriptコードにトランスパイルされます。

 
const ios = "iOS" /* IOS */;

Tree-shakingという観点においては、Union Types同様使われなければバンドルに含まれることはありません。

しかし長い文字列を割り当てたenumを使う場合、毎回展開されてしまうため、Union typesを使った場合に比べて多少不利になってくるのかなと思いました。 (以下はかなり極端なサンプルです)

 
// ts
const enum NAME {
  JUGEM = '寿限無寿限無五劫の擦り切れ海砂利水魚の…',
  TARO = '太郎',
  JIRO = '次郎',
}
const isJugem = name === NAME.JUGEM
 
// js トランスパイル後
const isJugem = name === "\u5BFF\u9650\u7121\u5BFF\u9650\u7121\u4E94\u52AB\u306E\u64E6\u308A\u5207\u308C\u6D77\u7802\u5229\u6C34\u9B5A\u306E\u2026" /* JUGEM */;

また、const enumはBabelでトランスパイルができなかったり、TypeScriptの --isolatedModules が有効になっている環境下ではあまり意味がない点で注意が必要です。
詳しくはKabukuさんのDevelopers Blogで解説されているので、そちらも合わせてご覧ください。

まとめ

列挙型をTree-shakingの観点で見た場合

Union Types > const enum > enum

の順でオススメします。

参考

Related Post