- Enum の代わりに union 型を使おうという風潮があるらしい
- TypeScript v3.4 から使える
const assertion
を用いると、使い勝手を損なわず Enum の代わりに union 型が使えるらしい
Enum とは
- 列挙型とも呼ばれる
- TS にはあって JS には無いもの、他の言語(C、C#、Java とか)にもある
- 定数をひとまとめに定義できる
enum Card {
Clubs, // 0
Diamonds, // 1
Hearts, // 2
Spades, // 3
}
const card: Card = Card.Hearts;
console.log(card); // 2
- 上記のように値には連番が振られる
- 意味のある分かりやすい値をもたせたい場合は、文字列や任意の数値を設定することもできる
enum Card {
Clubs = "clubs",
Diamonds = "diamonds",
Hearts = "hearts",
Spades = "spades",
}
console.log(Card.Diamonds); // diamonds
- enum は型と値の役割を兼ねるので、次のような書き方ができる
enum Color {
None,
Red,
Black,
}
const getCardColor = (card: Card): Color => {
if ([Card.Diamonds, Card.Hearts].some((v) => v === card)) {
return Color.Red;
}
if ([Card.Clubs, Card.Spades].some((v) => v === card)) {
return Color.Black;
}
return Color.None;
};
const color = getCardColor(Card.Clubs); // Color.Black
- 単純に文字列を使用するより、エディタを使ったリファクタが楽になる点もうれしい
Enum は使わないほうが良いよという話
- 使わないほうが良い理由は、以下サイトに説明されている
- これらを読んで思ったこと
- 「使用されてない Enum が Tree-shaking で除去されない」のは気持ち悪いけど、使用してないのはコンパイルオプションや lint でコケさせるので実害は無さそう
const enum
使ったことなかったけど、コンパイルオプションとの組み合わせ次第では、ハマるみたいなので、使うなら普通の enum で良さそう
- enum をアプリの外部に公開する意図が無く、普通の使い方をしてる分には使っても実害はなさそう
Enum の代わりに union 型を使う
- Enum の代わりに union 型を使うことを推奨されている(twitter や英語圏の記事とかで時々見かける)
- 前述のサンプルを union 型で書くと以下ようなコードが思いつくけど、typo しやすそうだし、リファクタもしずらそうでなんか嫌だ
type Card = "clubs" | "diamonds" | "hearts" | "spade";
type Color = "none" | "red" | "black";
const getCardColor = (card: Card): Color => {
// ここの配列の中身はtypoしても検出されない
if (["diamonds", "hearts"].some((v) => v === card)) {
return "red";
}
// ここの配列の中身はtypoしても検出されない
if (["clubs", "spade"].some((v) => v === card)) {
return "black";
}
return "none";
};
const color = getCardColor("clubs"); // black
- しかし、オブジェクトリテラルに const assertion を併用すれば、ほぼほぼ enum と同じように書くことができる
const assertion とは
- 例えば、特定の値しか代入できないリテラル型の宣言は、以下のように書ける
type Foo = "foo";
const foo: Foo = "foo";
const foo2: Foo = "foo2"; // エラー
typeof
で変数に格納された値から、その値の型の判別はできるけど、この場合リテラル型とは判定されない
const foo: Foo = "foo";
type Foo = typeof foo; // string型
const foo2: Foo = "bar"; // foo 以外の値が設定できちゃう
- const assertion を使えうとリテラル型として扱われる
const foo: Foo = "foo" as const;
type Foo = typeof foo; // `foo`のみを設定できるリテラル型
const foo2: Foo = "bar"; // foo以外を設定したらエラー
const assertion を使って enum っぽく書く
as const
とtypeof
、keyof
演算子を使って、オブジェクトリテラルの型エイリアスを宣言する
const Card = {
Clubs: "clubs",
Diamonds: "diamonds",
Hearts: "hearts",
Spades: "spades",
} as const;
// 以下は type Card = "clubs" | "diamonds" | "hearts" | "spades" と同じ
type Card = typeof Card[keyof typeof Card];
const Color = {
None: 0,
Red: 1,
Black: 2,
} as const;
// 以下は type Color = 0 | 1 | 2 と同じ
type Color = typeof Color[keyof typeof Color];
- 上記の
typeof
、keyof
演算子を小分けして書くと、以下のような意味合いになる
// type CardType = {
// readonly Clubs: "clubs";
// readonly Diamonds: "diamonds";
// readonly Hearts: "hearts";
// readonly Spades: "spades";
// }
type CardType = typeof Card;
// type CardKey = "Clubs" | "Diamonds" | "Hearts" | "Spades"
type CardKey = keyof CardType;
// type Card = "clubs" | "diamonds" | "hearts" | "spades"
type Card = CardType[CardKey];
- これで enum と同じように利用できるようになる
const getCardColor = (card: Card): Color => {
if ([Card.Diamonds, Card.Hearts].some((v) => v === card)) {
return Color.Red;
}
if ([Card.Clubs, Card.Spades].some((v) => v === card)) {
return Color.Black;
}
return Color.None;
};
const color = getCardColor(Card.Clubs); // Color.Black
Enum っぽさにこだわらない書き方
- 定義値の変更時の保守性という意味では、エディタベースの自動修正ができない分、オブジェクトリテラルで書いた場合より劣るけど、以下のような書き方でもタイプセーフにはなる
const redCards = ["diamonds", "hearts"] as const;
const blackCards = ["clubs", "spades"] as const;
type Card = typeof redCards[number] | typeof blackCards[number];
const colors = ["none", "red", "black"] as const;
type Color = typeof colors[number];
const getCardColor = (card: Card): Color => {
if (redCards.some((v) => v === card)) {
return "red";
}
if (blackCards.some((v) => v === card)) {
return "black";
}
return "none";
};
const color = getCardColor("clubs"); // black
感想
- 普通な使い方してる分には、not string enum が一番楽な気がする...
- けど世の流れにのって union 型に切り替えていこうと思う