TypeScript で列挙型として利用できる Enum ですが、JavaScript にトランスパイルされたコードを見て「どうなってんの?」と思ったので調べてみました。
平易な言葉を心がけて書いてます 😉
ちなむと、
TreeShaking の観点から Enum より Union Types のほうがいいよね、という意見があります。
私個人としても Union Types 推しなので、この投稿はあくまでも参考程度に読んでもらえればありがたいです。
参考記事: [TypeScriptのenumを使わないほうがいい理由を、Tree-shakingの観点で紹介します
- LINE Engineering](https://engineering.linecorp.com/ja/blog/typescript-enum-tree-shaking)
トランスパイルされたコードを確認
TypeScript の Playground でサクッと見てみます。
元のコード
enum Size {
MEDIUM,
LARGE,
XLARGE
}
// 以下のようにして呼び出せる
console.log(Size[0]); // -> 'MEDIUM'
console.log(Size.XLARGE); // -> 2
JavaScript にトランスパイルされたコード
元の TypeScript のコードから以下のようにトランスパイルされました。
"use strict";
var Size;
(function (Size) {
Size[Size["MEDIUM"] = 0] = "MEDIUM";
Size[Size["LARGE"] = 1] = "LARGE";
Size[Size["XLARGE"] = 2] = "XLARGE";
})(Size || (Size = {}));
// TypeScript と同じく以下の書き方で呼び出せる。
console.log(Size[0]); // -> 'MEDIUM'
console.log(Size.XLARGE); // -> 2
このコードを以下のポイントで見ていきましょう。
- 即時関数
- 即時関数の引数
-
Size[Size["MEDIUM"] = 0] = "MEDIUM";
の記述の解読
即時関数
即時関数とは読んで字のごとく、定義したその場で実行される関数です。
即時関数について理解しようとするとき、逆に即時関数ではない関数のほうから考えると分かりやすいと思います。
以下の関数 square
は、仮引数 number
に渡ってきた値を二乗して返します。定義したあとであれば、すぐでなくても、それ以降の行で実行が可能です。
// 関数を定義
function square(number) {
return number * number;
}
...
// 関数を実行
square(5); // -> 25
即時関数は定義したその場で実行されます。上の square 関数でいうならば、定義したすぐ下の行で実行させるのも同じことです。
(function (number) {
return number * number
}(5)); // -> 25
即時関数の引数
上の即時関数の例で、最後の行の }(5));
の箇所で 5
と記述しているのは、この即時関数に渡される実引数のことです。つまり、ここでは即時関数の仮引数 number
に対して、5を渡しています。
この実引数は変数にすることも可能です。以下のコードでは、five
という変数に 5
を定義して、即時関数の実引数として渡したものです。
const five = 5;
(function (number) {
return number * number
}(five)); // -> 25
ここで enum からトランスパイルされたコードに戻ってみます。見やすいように即時関数内のコードを省略しました。
var Size;
(function (Size) {
Size[Size["MEDIUM"] = 0] = "MEDIUM";
})(Size || (Size = {}));
enum の名称として定義した Size
という名前が、即時関数の仮引数名と、実引数として渡される変数名のどちらにも使われています。これがちょっと読みにくくさせてますね。仮引数のほうを適当に arg
に置き換えて次に進みましょう。
var Size;
(function (arg) {
arg[arg["MEDIUM"] = 0] = "MEDIUM";
})(Size || (Size = {}));
これで幾分か読みやすくなりました。
実引数として渡された Size
に対して、即時関数の内部でメンバと値を追加している構成が分かりやすくなったと思います。
Size[Size["MEDIUM"] = 0] = "MEDIUM";
の記述の解読
実際の挙動から考えると、以下の式は、メンバの値かインデックスのどちらでも呼び出せるように記述されているはずです。
Size[Size["MEDIUM"] = 0] = "MEDIUM";
文脈から考えると [Size["MEDIUM"] = 0]
は 0
になるようです。別の例で考えてみます。
let a = 2;
変数 a は当然 2 ですね。
では以下の式のとき、変数 b の値はどうなるでしょうか。
let a = 2;
let b = (a = 3);
変数 b は 3 となります。ちなみに上書きされている a のほうも 3 になります。調子にのってもう一例。
orange = (pine = (peach = '桃'));
この結果、orange も pine も peach も 桃
という値になります。何だか気持ち悪いですね。でもこの特性を使うことで enum のトランスパイルの結果は表現されています。
Size[Size["MEDIUM"] = 0] = "MEDIUM";
// 以下にてこの式を分解してみる。
let a = (Size["MEDIUM"] = 0);
// a は 0 となる。
Size[a] = "MEDIUM";
// つまり Size[0] = "MEDIUM" という式と同じ。
この式によって、Size のメンバの値かインデックスの双方から呼び出せるようにしているというわけでした。
ここまで。