TypescriptのEnum型の代わりにUnion型を使用する

この投稿は、弊社が提供するWESEEK TECH通信の一環です。
WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。

はじめに

こんにちは、システムエンジニアのかおりです。この記事では、TypeScriptで用いられる Enum型 とUnion型の基本的な使い方から、Enumを避けるべきと言われている理由、Union型を用いてEnumのように書く方法などを説明しています。

Enum (列挙型) とは?

そもそも Enum とは何か、なんのために使うものなのでしょうか。

ここで説明している Enum とは総称であり、列挙型とも呼ばれます。以下の例は TypeScript のコードですが、他の言語でも存在する概念・実装であり、複数の定数を一つにまとめて定義したり管理したりすることができます。

enum SIZE {
  Small,
  Medium,
  Large,
}

const newSize = SIZE.Small;

console.log(newSize === SIZE.Small);  // true
console.log(newSize === SIZE.Large);  // false

Enum (列挙型) を使うメリット

上記の例、単純に文字列として "small" などを変数で定義して比較する方法と比べてどんなメリットがあるのでしょうか。

最も大きな恩恵は、スペルミスや大文字小文字などの人的エラーを防ぐことができる点です。

例えば Visual Studio Code などの TypeScript の補完が可能なエディタ上で SIZE. と入力すると、 enum SIZE で定義されているキーがサジェストされます。

また、Enumは型としても利用できるので、より堅牢なシステムの構築に役立ちます。

JavaScript、TypeScript での enum

JavaScript には enum は存在しません。そのため、enum でやりたいことを実現するには const が代用となるでしょう。

  const SIZE = {
    Small: 'small',
    Medium: 'medium',
    Large: 'large',
  };
  console.log(SIZE.Small); // small

一方 TypeScript には enum の機能があります。しかし、以下のように少々癖があります。

// 何も指定しない場合、0からの番号が割り振られていきます。
enum SIZE {
  Small, // 0
  Medium, // 1
  Large,  // 2
}
console.log(SIZE.Large);  // 2
console.log(SIZE[1]); // Medium

少し冗長ですが、各定数に任意の文字列任意の数字を割り当てることもできます。

enum SIZE {
  Small = "small",
  Medium = "medium",
  Large = "large",
}
console.log(SIZE.Large);  // large
// 一番最初の定数(ここではSmall)に5を指定した場合、定数は5からの数字が割り振られます。
enum SIZE {
  Small = 5, // 5
  Medium, // 6
  Large,  // 7
}
console.log(SIZE.Large);  // 7

定数の途中で数字を指定した場合、最初の定数は0から始まり、途中から指定された文字から数字が割り振られます。

enum SIZE {
  Small, // 0
  Medium = 4, // 4
  Large,  // 5
}
console.log(SIZE.Medium) // 4

Enum 型 と Union型の比較について

これまで述べたメリットを踏まえた上で、実は界隈では TypeScript の enum については利用の反対派(非推奨派?)が存在します。
このenum型と、非推奨派が推奨するunion型についてお話ししたいと思います。

Enum を使用する際の注意点

1. 意図しない値にアクセスできてしまう

上記の説明で、enumに数値が割り当てられている場合、その数値から値を取得することができると説明しました。これを利用して、割り当てられていない数値からの値の取得が可能となり、意図せずundefinedが返ってきてしまいます。

enum SIZE {
  Small,
  Medium,
  Large,
}

  console.log(SIZE[1]);    // Medium
  // no error!!
  console.log(SIZE[5]);    // undefined

数値のenumにはあらゆるnumberを割り当てることができるので、型安全性に欠けてしまいます。

2. Tree-shaking が作動せず、呼び出されない不要なコードがコンパイルされてしまう

TypeScriptで定義されたenumは、Javascriptにトランスパイルされる際に以下のような即時実行関数として変換されます。即時実行関数とは、「定義されるとすぐに実行される」JavaScriptの関数のことです。

var SIZE;
(function (SIZE) {
    SIZE[SIZE["Small"] = 0] = "Small";
    SIZE[SIZE["Medium"] = 4] = "Medium";
    SIZE[SIZE["Large"] = 5] = "Large";
})(SIZE || (SIZE = {}));

// 文字列を割り当てた場合
var SIZE;
(function (SIZE) {
    SIZE2["Small"] = "small";
    SIZE2["Medium"] = "medium";
    SIZE2["Large"] = "large";
})(SIZE || (SIZE = {}));

このように即時実行関数としてトランスパイルされると、Tree-shakingが上手く働かなくなってしまうという問題点があります。

  • Tree-shakingとは....?
    Tree-shaking とは、webpackなどのモジュールハンドラがJavascriptファイルを一つにまとめる際に、実行されない余分なコードを削除するといった処理のことです。
    木を振ることで、不要なものを落としてボリュームを小さくするイメージだと個人的に解釈しました。

以下のサイトで Tree shakingについて説明されているので、よければご覧ください。

3. isolatedModulesがtrueの場合、const enum使用時にコンパイルエラーになることがある

--isolatedModulesオプションを有効にする場合、

Transpile each file as a separate module (similar to 'ts.transpileModule').
このオプションをtrueにした場合、const enumはエラーとなりコンパイルできない。

詳細は以下の記事をご覧ください。
https://www.kabuku.co.jp/developers/good-bye-typescript-enum

以上の理由から、enumの利用は避けるべきであると言われています。
では、enumのような機能を使いたい場合はどうすれば良いのでしょうか?
それは、Union型を使うことで実現できます。

Union型について

Union型は、以下のように2つ以上の型をパイプ記号|で繋げて書くことで、複数の型を指定できます。

const middleName: string | null

Enumの代わりに、Union型を用いる

Enumを使わずとも、Enumのように定義するにはどうすればいいのでしょうか?
const assertion(as const), keyof, typeof を使ってenumのような定義をすることができます。

// const assertionを使って、リテラル型として扱う
const SizeType = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large',
} as const;

// type SizeType = 'small' | 'medium' | 'large'
type SizeType = typeof SizeType[keyof typeof SizeType];

細かく分解すると、以下のようになります。

  //   type SizeType = {
  //     readonly SMALL: "small";
  //     readonly MEDIUM: "medium";
  //     readonly LARGE: "large";
  // }
  type SizeType = typeof Size;

  // type SizeKey = "SMALL" | "MEDIUM" | "LARGE"
  type SizeKey = keyof SizeType;

  // type Size = 'small' | 'medium' | 'large'
  type Size = SizeType[SizeKey];

const assertion、リテラル型, keyof, typeof に関しては、以下の記事でわかりやすく説明されているので、よければご確認ください。

unionのリストを手に入れたい場合

javaScriptが提供する Object.values関数を使用して、取得することができます。

// const AllSizeType = Object.values(SizeType);
const AllSizeType = Object.values(SizeType);

完成系テンプレート

WESEEK では以下をテンプレートとして使用しています。const assertion に値を追加するだけで union と全値のリストを export できるので便利です。

const SizeType = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large',
} as const;

// type SizeType = "small" | "medium" | "large"
export type SizeType = typeof SizeType[keyof typeof SizeType];
// 全てのtypeを配列として取得
// const AllSizeType: ("small" | "medium" | "large")[]
export const AllSizeType = Object.values(SizeType);

まとめ

  • 総称としての Enum (列挙型)について
    • 複数の定数をまとめて定義したり管理するのに便利
    • いろんな言語で利用できるが JavaScript にはない
    • Enum は型としても使えるので、バグを防ぐことに役立つ
  • TypeScript の enum vs union
    • いろんな記述方法がある
    • enum は、記述は簡単
    • enum の利用には複数の落とし穴があるので、union型を用いることが一部では推奨されている

こだわりがなければ、TypeScript ではenum型ではなくunion型を用いることをお奨めします。

参考にさせていただいたサイト