💡

一日一処: TypeScriptのEnumが生み出すJavaScriptの構文を眺める

2024/02/07に公開

Enumとは

Enumはいくつかの言語で使用できる列挙型と呼ばれるものだ。いわゆる、通常の配列に近いが、内容は、Mapのように唯一無二で重複はない。そして、それ自体は、文字列や数列とはことなり、シンボルのような取り扱いとなる。ただ、JavaScriptには、このEnumの概念は存在しない。
全く関係ない、C#でのEnumを見てみる。

enum Season
{
    Spring,
    Summer,
    Autumn,
    Winter
}

宣言は簡単で、Seasonという型を作り出し、その種類として、4つのシンボルがある、といった具合だ。TypeScriptでの近い存在といえば、ユニオン型ではないだろうか。
このような列挙型をC#ではこの様に使用する。型として、成立しているこのEnumは、挿入される値が決まっており、それに合わせて処理を行う。

public class Program
{
	public static void Main()
	{
		Season s = Season.Spring;
		if (s == Season.Spring) {
			Console.WriteLine('春');
		}
	}
}

Enumの存在とその意義

TypeScriptはJavaScriptとの互換を保つため、JavaScriptの言語概念そのものを大きく捻じ曲げるような仕様は追加されることは少ない。ただ、異例の存在として、Enumが存在する。前述のとおり、ユニオン型などで、列挙型を模倣すれば問題ないが、それだと、String型となってしまうことが多いため、やはり、C#の例の様に、正確な列挙型を用いるほうがわかりやすい場面もある。個人的には、わざわざEnumは使用せず、結局文字列のユニオン型を用いるが。

TSにおけるEnumがどうJSに変換されるか

そんなときは、みんな大好きPlaygroundを確認するのだ。まずは、先ほどのC#と同じコードをTypeScriptで表現する。Playgroundでは、このコードを自動的にJavaScriptへ変換してくれる。

enum Season
{
    Spring,
    Summer,
    Autumn,
    Winter
}

const season: Season = Season.Spring
if (season === Season.Spring) {
    console.log('春')
}

変換されたJavaScriptはこれだ。

"use strict";
var Season;
(function (Season) {
    Season[Season["Spring"] = 0] = "Spring";
    Season[Season["Summer"] = 1] = "Summer";
    Season[Season["Autumn"] = 2] = "Autumn";
    Season[Season["Winter"] = 3] = "Winter";
})(Season || (Season = {}));
const season = Season.Spring;
if (season === Season.Spring) {
    console.log('春');
}

実に面白い。なんとなくEnumの原型は残しているようにも見えるが、実際は、JavaScriptの基礎的な記述のみで成り立っている。後半の変数宣言以降は特に解説などは不要だろう。Enumが変換された箇所のみフォーカスしてみる。

var Season;
(function (Season) {
    // 中略
})(Season || (Season = {}));

まず、SeasonのEnumとしての名称を変数宣言している。後に、無名関数を即時実行を行っている。簡単な構文は、以下のようなもので、関数を括弧でくくり、後に関数にわたす引数を記述する。この例だと、'test'の文字列を関数のargに渡している。

(function(arg) {})('test')

注目すべきは、Season || (Season = {})の箇所で、即時実行を行う関数に渡す引数は、Seasonだが、変数宣言した直後なので、初回はundefinedが挿入されている。その場合は、||の判定により、後者のSeason = {}が引数として渡される。空のオブジェクトを変数に格納し、それをそのまま引数として渡すことになる。つまり、変数の初期化がここで行われている。
無名関数の内部では、以下のことが行われている。

    Season[Season["Spring"] = 0] = "Spring";
    Season[Season["Summer"] = 1] = "Summer";
    Season[Season["Autumn"] = 2] = "Autumn";
    Season[Season["Winter"] = 3] = "Winter";

これも初見だと複雑なことをしているように見えるが、先程の代入と初期化と同様のことが行われている。Season[Season["Spring"] = 0] = "Spring";では、Seasonの何かを参照し、それぞれの文字列("Spring"など)を挿入している。そのなにかとは、Season["Spring"] = 0だ。結局のところ、Springでは、ここで代入されている'0'番目に値が渡されることになる。Season[0] = "Spring"と同義だ。ただし、これのままだと、変換されずに残っているSeason.Springの参照には、効力を発揮しない。そこで、Season["Spring"] = 0とすることで、Season.Springを参照すると、0という値を得ることができる。

ここまで見て分かる通り、結局のところ、オブジェクトで、定義しているようなものと大して変わらない。Enumとしての存在意義は、文字列ユニオン型やオブジェクト定数のようなパラメータの定義よりは簡潔で、シンプルに書けるというだけに過ぎない。様々な思想や開発方法が存在するが、denoのように、変換を前提ではなく、TypeScriptを直接実行できる場合は、意義があるかもしれないが、JavaScriptに変換して実行する環境下では、その他の手段が最適に思える。

ただし、TypeScriptを使う身としては、最終的にJavaScriptへどの様に変換されるかという点については、知っておいても損はしないだろう。

Discussion