Enum の代わりに使う union 型

  • 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 の代わりに 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 consttypeofkeyof演算子を使って、オブジェクトリテラルの型エイリアスを宣言する
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];
  • 上記のtypeofkeyof演算子を小分けして書くと、以下のような意味合いになる
// 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 型に切り替えていこうと思う