LoginSignup
14
6

More than 5 years have passed since last update.

TypeScript Handbook を読む (7. Enums)

Last updated at Posted at 2017-03-16

TypeScript Handbook を読み進めていく第七回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums (今ココ)
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Enums

原文

Enums

Enum を使用すると名前付きの定数群を定義することができます。
TypeScript では数値ベースの enum と文字列ベースの enum の両方を提供しています。

Numeric enums

まず数値 enum から見ていきますが、これは他の言語を使用していればなじみが深いでしょう。
enum は enum キーワードを使って宣言します。

TypeScript
enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
JavaScript
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 = {}));

上記の例は Up1 として初期化されている数値 enum となります。
他のメンバの値は自動的にインクリメントして定義されます。
つまり、Direction.Up1Down2Left3Right4 となります。

初期値を指定しないことも可能です。

TypeScript
enum Direction {
    Up,
    Down,
    Left,
    Right,
}
JavaScript
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 = {}));

この場合、Up0Down1、といった具合に初期化されます。
この自動採番の仕組みは、同じ enum 内で値が一意である必要があるものの、値そのものは何でも良いような場合に役立ちます。

enum の使用方法は単純です。
メンバを使用する場合は単にプロパティのようにアクセスするだけで、型を宣言する場合は enum の名前を使用します。

TypeScript
enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

数値 enum は 計算された値と定数 を混在させることが可能です。
簡単に言うと、初期値を指定しないメンバは一番最初か、数値定数または他の定数 enum メンバで初期化されたメンバの後でしか宣言できません。
つまり、以下のように宣言することはできません。

TypeScript
enum E {
    A = getSomeValue(),
    B, // エラー! 'A' は定数で初期化されていないため、'B' には初期値が必要
}

String enums

文字列 enum は数値 enum と似ていますが、少しだけ 実行時の振る舞いに違い があります。
文字列 enum のメンバはすべて、文字列リテラルか、他のメンバで初期化する必要があります。

TypeScript
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}
JavaScript
var Direction;
(function (Direction) {
    Direction["Up"] = "UP";
    Direction["Down"] = "DOWN";
    Direction["Left"] = "LEFT";
    Direction["Right"] = "RIGHT";
})(Direction || (Direction = {}));

文字列 enum は自動採番の仕組みを持っていませんが、うまく "文字列化" できるというメリットがあります。
どういうことかと言うと、例えばデバッグ中に数値 enum の値を見ても意味のある情報は得られにくい (なので 逆マッピング が役に立ちます) ですが、文字列 enum であれば、メンバの名前以外にわかりやすい情報が得られます。

要は 01 より UPDOWN の方が分かりやすいよね、ということ

Heterogeneous enums

これが必要になることがあるとは思いませんが、enum は文字列と数値のメンバを混ぜて定義することも可能です。

TypeScript
enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}
JavaScript
var BooleanLikeHeterogeneousEnum;
(function (BooleanLikeHeterogeneousEnum) {
    BooleanLikeHeterogeneousEnum[BooleanLikeHeterogeneousEnum["No"] = 0] = "No";
    BooleanLikeHeterogeneousEnum["Yes"] = "YES";
})(BooleanLikeHeterogeneousEnum || (BooleanLikeHeterogeneousEnum = {}));

ただし、JavaScript の実行時の振る舞いを活用するのでなければ、これを避けることをお勧めします。

Computed and constant members

enum のメンバは 定数 または 計算された値 のいずれかの値を持ちます。
以下のいずれかに該当する場合、enum のメンバは定数とみなされます。

  • 最初のメンバであり、かつ、初期値が指定されていない場合
    この場合、値として 0 が設定されます。
TypeScript
// E.X は定数
enum E { X }
  • 初期値が指定されておらず、かつ、前のメンバが 数値定数 の場合
    この場合、前のメンバの値+1 が値として設定されます。
TypeScript
// 'E1'、'E2' のメンバはすべて定数

enum E1 { X, Y, Z }

enum E2 {
    A = 1, B, C
}
  • 初期値が定数 enum 式の場合
    定数 enum 式とはコンパイル時に評価可能な定数式のことで、以下のいずれかになります。
    • リテラル enum 式 (基本的には文字列リテラルまたは数値リテラル)
    • 定義済みの enum のメンバ (他の enum のメンバでも可)。
    • 括弧で囲まれた定数 enum 式
    • 単項演算子 (+-~) が指定された定数 enum 式
    • 二項演算子 (+-*/%<<>>>>>&|^) を含む定数 enum 式
      ただし、評価の結果が NaN または Infinity になる場合はコンパイルエラーになります。

上記のいずれにも該当しない場合、enum のメンバは計算された値とみなされます。

TypeScript
enum FileAccess {
    // 定数
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // 計算された値
    G = "123".length
}
JavaScript
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)

すべてのメンバがリテラルメンバの場合、いくつかの特殊構文を使用できます。

ひとつ目の構文として、メンバを型としても使用できるようになります。

TypeScript
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,
}
JavaScript
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 に存在する値の集合と考えてください。
これにより、値を比較する時に犯しがちなバグを防ぐことができます。

TypeScript
enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
        //             ~~~~~~~~~~~
        // エラー! 'E.Foo' と 'E.Bar' には '!==' を適用することはできない。
    }
}

この例では、最初に xE.Fooない ことをチェックしています。
これが成り立つ場合、|| が短絡評価されて if 文の中が実行されます。
逆に、この条件が成り立たなかった場合には x の値は E.Foo であるということです。
そのため、xE.Bar と等しいかどうかの比較は意味をなさないということです。

Enums at runtime

enum は実行時にも存在する、実オブジェクトです。

interface と違ってちゃんと JavaScript のオブジェクトになるということね

つまり、以下のような enum は、

TypeScript
enum E {
    X, Y, Z
}

実際に次のように関数に渡すことができます。

TypeScript
function f(obj: { X: number }) {
    return obj.X;
}

// 'E' は数値型の 'X' プロパティを持つため、これは動作する。
f(E);

Reverse mappings

enum はメンバと同じ名前のプロパティを持つオブジェクトとして生成されるだけでなく、数値メンバについてはメンバの値からメンバ名を取得するための 逆マッピング も生成されます。

TypeScript
enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
JavaScript
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 修飾子を付けることで定義します。

TypeScript
const enum Enum {
    A = 1,
    B = A * 2
}

定数 enum では定数 enum 式しか使用することができません。
また、通常の enum と違い、コンパイル時に完全に取り除かれます。
そして、定数 enum へのアクセスはインライン化されます。
これが計算された値を定数 enum の初期値にできない理由です。

TypeScript
const enum Directions {
    Up,
    Down,
    Left,
    Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
JavaScript
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

Ambient enums

アンビエント enum は既存の enum の形式を宣言するために使用します。

TypeScript
declare enum Enum {
    A = 1,
    B,
    C = 2
}

console.log(Enum.A, Enum.B);
JavaScript
console.log(Enum.A, Enum.B);

通常の enum であれば、初期値が指定されておらず、かつ、前のメンバが定数とみなされている場合にはそのメンバも定数とみなされるのに対し、アンビエント (かつ非定数) enum では初期値を持たないメンバは 常に 計算された値とみなされます。

イマイチ使い所が分からないけど、他のところで定義している enum にどんなメンバが存在するかを宣言するために使うイメージかな。
あとは初期値を指定していないメンバの扱いの違いだけど、switch 文に使えるとかその辺りが変わってくるのかな?

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6