[v4系]TypeScriptでenumを実現する
TypeScriptでenum(列挙型)は積極的には使わない方が良い、と調べるとしばしば出てきます。「自分でもまとめよう」と思いplaygroundで試したところ、なんと出るはずのコンパイルエラーが発生しません。どうやらv5のアップデートで(一部?)対応が入ったようです。
とはいえ、まだまだv4系を使っているサービスもたくさんあると思います。v4系で動作確認したものをまとめようと思います。
動作確認環境
- Playground
- v4.9.5
TypeScriptのenum
TypeScriptでは文字ベースのenum=文字列列挙型、と数値ベースのenum=数値列挙型を定義して使うことができます。
enum Direction {
Top = 0,
Right = 1,
Bottom = 2,
Left = 3
}
const canMove = (direction: Direction) => {
if(direction === Direction.Top) {
return true;
}
return false;
}
console.log(canMove(Direction.Bottom)); // false
TypeScriptのenumの懸念点
数値列挙型には型安全上の問題点があります。
以下のように、0から3の値しか持たないはずの数値列挙型に、それ以外の値を入れてもコンパイルエラーが発生しません。
enum Direction {
Top = 0,
Right = 1,
Bottom = 2,
Left = 3
}
const direction: Direction = 10;
console.log(direction); // 10
よって、関数の引数の型として数値列挙型を指定してたとしても、コンパイルエラーなしで列挙型で定義した値以外を引数にとることができます。
enum Direction {
Top = 0,
Right = 1,
Bottom = 2,
Left = 3
}
const canMove = (direction: Direction) => {
if(direction === Direction.Top) {
return true;
}
return false;
}
console.log(canMove(10)); // false
また、列挙型に値を指定することでメンバー名を得ることもできますが、その時も列挙型に存在しない値を指定してもコンパイルエラーが発生しません。
enum Direction {
Top = 0,
Right = 1,
Bottom = 2,
Left = 3
}
console.log(Direction[0]); // "Top"
console.log(Direction[10]); // undefined
TypeScriptのenumの代替案
Union型
シンプルな代替案です。以下のようにenumの代わりにUnion型を使います。
type Direction = "Top" | "Right" | "Bottom" | "Left";
const canMove = (direction: Direction) => {
if(direction === "Top") {
return true;
}
return false;
}
console.log(canMove("Top")); // true
canMove
関数の中のif文で direction === "Top"
としています。ここでDirection
に定義している値以外を入れようとするとコンパイルエラーが発生します。
canMove
関数呼び出し時の引数の指定も同様です。
型安全という点では良さそうです。ですがDirection
を別ファイルでも使いたい、などのように使う範囲が広がってくると、直接String値を使わなければならない部分が個人的にはすごく気になります。
オブジェクトリテラル
オブジェクトリテラルを使うことで、直接Stringの値を使う必要がなくなります。
const Direction = {
Top: 0,
Right: 1,
Bottom: 2,
Left: 3,
} as const;
type Direction = typeof Direction[keyof typeof Direction];
const canMove = (direction: Direction) => {
if(direction === Direction.Top) {
return true;
}
return false;
}
console.log(canMove(Direction.Top)); // true
少し詳細に説明をします。
const Direction = {
Top: 0,
Right: 1,
Bottom: 2,
Left: 3,
} as const;
as const
をつけることで、オブジェクトのプロパティがreadonlyになり、リテラルタイプで指定した値と同等とみなされます。
as const
あり
as const
なし
as const
なしの方は、各プロパティにreadonly
が付いておらず、型もnumber
と推測されています。一方、as const
ありの方は、各プロパティにreadonly
が付き、型がそれぞれに指定したリテラル値となっています。
as const
にすることによってプロパティをreadonly
にすることができるため、プロパティの値を書き換えられることを防げます。
続いて、以下のコードによって、結果としてtype Direction
は 0 | 1 | 2 | 3
のUnion型となります。
type Direction = typeof Direction[keyof typeof Direction];
// type Direction = 0 | 1 | 2 | 3
コードの右側から解説です。
type DirectionType = typeof Direction;
// type DirectionType = {
// readonly Top: 0;
// readonly Right: 1;
// readonly Bottom: 2;
// readonly Left: 3;
// }
type KeyofType = keyof DirectionType;
// type KeyofType = "Top" | "Right" | "Bottom" | "Left"
type IndexedAccessType = DirectionType[KeyofType];
// type IndexedAccessType = 0 | 1 | 2 | 3
利用している演算子等々は以下が参考になるかと思います。
オブジェクトリテラルを利用することによって、Union型を利用しつつ、Direction.Top
のようにアクセスすることも可能になります。
Discussion