TypeScript Handbook を読み進めていく第七回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics
- Enums (今ココ)
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Enums
Enums
Enum を使用すると名前付きの定数群を定義することができます。
TypeScript では数値ベースの enum と文字列ベースの enum の両方を提供しています。
Numeric enums
まず数値 enum から見ていきますが、これは他の言語を使用していればなじみが深いでしょう。
enum は enum
キーワードを使って宣言します。
enum Direction {
Up = 1,
Down,
Left,
Right
}
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
上記の例は Up
が 1
として初期化されている数値 enum となります。
他のメンバの値は自動的にインクリメントして定義されます。
つまり、Direction.Up
は 1
、Down
は 2
、Left
は 3
、Right
は 4
となります。
初期値を指定しないことも可能です。
enum Direction {
Up,
Down,
Left,
Right,
}
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
この場合、Up
は 0
、Down
は 1
、といった具合に初期化されます。
この自動採番の仕組みは、同じ enum 内で値が一意である必要があるものの、値そのものは何でも良いような場合に役立ちます。
enum の使用方法は単純です。
メンバを使用する場合は単にプロパティのようにアクセスするだけで、型を宣言する場合は enum の名前を使用します。
enum Response {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: Response): void {
// ...
}
respond("Princess Caroline", Response.Yes)
数値 enum は 計算された値と定数 を混在させることが可能です。
簡単に言うと、初期値を指定しないメンバは一番最初か、数値定数または他の定数 enum メンバで初期化されたメンバの後でしか宣言できません。
つまり、以下のように宣言することはできません。
enum E {
A = getSomeValue(),
B, // エラー! 'A' は定数で初期化されていないため、'B' には初期値が必要
}
String enums
文字列 enum は数値 enum と似ていますが、少しだけ 実行時の振る舞いに違い があります。
文字列 enum のメンバはすべて、文字列リテラルか、他のメンバで初期化する必要があります。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
Direction["Left"] = "LEFT";
Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));
文字列 enum は自動採番の仕組みを持っていませんが、うまく "文字列化" できるというメリットがあります。
どういうことかと言うと、例えばデバッグ中に数値 enum の値を見ても意味のある情報は得られにくい (なので 逆マッピング が役に立ちます) ですが、文字列 enum であれば、メンバの名前以外にわかりやすい情報が得られます。
要は
0
、1
よりUP
、DOWN
の方が分かりやすいよね、ということ
Heterogeneous enums
これが必要になることがあるとは思いませんが、enum は文字列と数値のメンバを混ぜて定義することも可能です。
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
var BooleanLikeHeterogeneousEnum;
(function (BooleanLikeHeterogeneousEnum) {
BooleanLikeHeterogeneousEnum[BooleanLikeHeterogeneousEnum["No"] = 0] = "No";
BooleanLikeHeterogeneousEnum["Yes"] = "YES";
})(BooleanLikeHeterogeneousEnum || (BooleanLikeHeterogeneousEnum = {}));
ただし、JavaScript の実行時の振る舞いを活用するのでなければ、これを避けることをお勧めします。
Computed and constant members
enum のメンバは 定数 または 計算された値 のいずれかの値を持ちます。
以下のいずれかに該当する場合、enum のメンバは定数とみなされます。
- 最初のメンバであり、かつ、初期値が指定されていない場合
この場合、値として0
が設定されます。
// E.X は定数
enum E { X }
- 初期値が指定されておらず、かつ、前のメンバが 数値定数 の場合
この場合、前のメンバの値+1 が値として設定されます。
// 'E1'、'E2' のメンバはすべて定数
enum E1 { X, Y, Z }
enum E2 {
A = 1, B, C
}
- 初期値が定数 enum 式の場合
定数 enum 式とはコンパイル時に評価可能な定数式のことで、以下のいずれかになります。- リテラル enum 式 (基本的には文字列リテラルまたは数値リテラル)
- 定義済みの enum のメンバ (他の enum のメンバでも可)。
- 括弧で囲まれた定数 enum 式
- 単項演算子 (
+
、-
、~
) が指定された定数 enum 式 - 二項演算子 (
+
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
) を含む定数 enum 式
ただし、評価の結果がNaN
またはInfinity
になる場合はコンパイルエラーになります。
上記のいずれにも該当しない場合、enum のメンバは計算された値とみなされます。
enum FileAccess {
// 定数
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 計算された値
G = "123".length
}
var FileAccess;
(function (FileAccess) {
// 定数
FileAccess[FileAccess["None"] = 0] = "None";
FileAccess[FileAccess["Read"] = 2] = "Read";
FileAccess[FileAccess["Write"] = 4] = "Write";
FileAccess[FileAccess["ReadWrite"] = 6] = "ReadWrite";
// 計算された値
FileAccess[FileAccess["G"] = "123".length] = "G";
})(FileAccess || (FileAccess = {}));
Union enums and enum member types
定数メンバの特殊版としてリテラルメンバがあります。
これは初期値を持たないか、値が初期値として指定されたメンバを指します。
- 文字列リテラル (例:
"foo"
,"bar"
,"baz"
) - 数値リテラル (例:
1
,100
) - マイナス記号付きの数値リテラル (例:
-1
,-100
)
すべてのメンバがリテラルメンバの場合、いくつかの特殊構文を使用できます。
ひとつ目の構文として、メンバを型としても使用できるようになります。
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// ~~~~~~~~~~~~~~~~ エラー!
radius: 100,
}
var ShapeKind;
(function (ShapeKind) {
ShapeKind[ShapeKind["Circle"] = 0] = "Circle";
ShapeKind[ShapeKind["Square"] = 1] = "Square";
})(ShapeKind || (ShapeKind = {}));
var c = {
kind: ShapeKind.Square,
// ~~~~~~~~~~~~~~~~ エラー!
radius: 100,
};
それ以外に、enum の型自身が各メンバの 共用型
になります。
共用型 についてはまだ説明していませんが、ここでは enum に存在する値の集合と考えてください。
これにより、値を比較する時に犯しがちなバグを防ぐことができます。
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ~~~~~~~~~~~
// エラー! 'E.Foo' と 'E.Bar' には '!==' を適用することはできない。
}
}
この例では、最初に x
が E.Foo
で ない ことをチェックしています。
これが成り立つ場合、||
が短絡評価されて if 文の中が実行されます。
逆に、この条件が成り立たなかった場合には x
の値は E.Foo
であるということです。
そのため、x
が E.Bar
と等しいかどうかの比較は意味をなさないということです。
Enums at runtime
enum は実行時にも存在する、実オブジェクトです。
interface と違ってちゃんと JavaScript のオブジェクトになるということね
つまり、以下のような enum は、
enum E {
X, Y, Z
}
実際に次のように関数に渡すことができます。
function f(obj: { X: number }) {
return obj.X;
}
// 'E' は数値型の 'X' プロパティを持つため、これは動作する。
f(E);
Reverse mappings
enum はメンバと同じ名前のプロパティを持つオブジェクトとして生成されるだけでなく、数値メンバについてはメンバの値からメンバ名を取得するための 逆マッピング も生成されます。
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"
生成されたコードでは enum は順方向 (name
-> value
) と逆方向 (value
-> name
) の両方のマッピングを持つオブジェクトにコンパイルされます。
enum への参照は常にプロパティ経由のアクセスとして出力され、インライン化されることはありません。
const
enums
ほとんどのケースにおいて、enum は完璧な解決方法と言えます。
ただし、余分な生成コードや enum の値への間接的なアクセスのコストを払いたくないこともあるでしょう。
そのような場合には const
enum を使用することができます。
定数 enum は enum に const
修飾子を付けることで定義します。
const enum Enum {
A = 1,
B = A * 2
}
定数 enum では定数 enum 式しか使用することができません。
また、通常の enum と違い、コンパイル時に完全に取り除かれます。
そして、定数 enum へのアクセスはインライン化されます。
これが計算された値を定数 enum の初期値にできない理由です。
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
Ambient enums
アンビエント enum は既存の enum の形式を宣言するために使用します。
declare enum Enum {
A = 1,
B,
C = 2
}
console.log(Enum.A, Enum.B);
console.log(Enum.A, Enum.B);
通常の enum であれば、初期値が指定されておらず、かつ、前のメンバが定数とみなされている場合にはそのメンバも定数とみなされるのに対し、アンビエント (かつ非定数) enum では初期値を持たないメンバは 常に 計算された値とみなされます。
イマイチ使い所が分からないけど、他のところで定義している enum にどんなメンバが存在するかを宣言するために使うイメージかな。
あとは初期値を指定していないメンバの扱いの違いだけど、switch 文に使えるとかその辺りが変わってくるのかな?