TypeScript の列挙型と値型

最終更新: 2021-02-20 06:14:56 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

閉じた集合には、コンパイル時の制約、実行時の検索オブジェクト、ワイヤ表現、または検証済みの振る舞いを持つドメインオブジェクトが必要になる場合があります。これらの要件は同一ではないため、TypeScript には異なる表現が用意されています。

型の要件と実行時の同一性を分離する #

次のような型は、:

ts
type Status = "pending" | "running" | "done";

型チェック中にのみ存在します。JavaScript は Status を列挙することも、未知の文字列がそれに属するかを検査することも、メタデータを付加することもできません。実行時コードには、対応する値または検証関数が必要です。

一方、ネイティブの enum は JavaScript オブジェクトを出力し、enum 型を導入します。as const テーブルは通常のオブジェクトを出力し、そこから型を導出します。クラスはコンストラクターを出力し、不変条件を適用できます。実際にプログラムが必要とする実行時の同一性に応じて選択してください。

ネイティブ enum の出力を理解する #

数値 enum は数値を割り当て、順方向と逆方向のマッピングを出力します。

ts
enum StatusCode {
  Pending,
  Running,
  Done,
}

console.log(StatusCode.Running); // 1
console.log(StatusCode[1]);      // "Running"

生成されたオブジェクトは、名前から数値、および数値から名前への両方向の検索をサポートします。文字列 enum は、名前から値へのプロパティのみを出力します。

ts
enum Status {
  Pending = "pending",
  Running = "running",
  Done = "done",
}

TypeScript 5.0 以降、計算された値から初期化されたメンバーを含め、すべての enum メンバーは個別のメンバー型を持ちます。これにより、絞り込みと網羅的な switch が可能になりますが、出力されるオブジェクトは変わりません。

const enum は、メンバー値をインライン化して enum を消去するよう TypeScript に要求します。

ts
const enum Direction {
  Left,
  Right,
}

インライン化は、isolated-module トランスパイラーや、コンシューマーのコンパイルに使われた正確な enum 値よりも長く存続する公開済み宣言を持つライブラリと衝突する可能性があります。ツールチェーン全体とバージョニングポリシーを管理できない限り、パッケージ境界を越えて ambient const enum 宣言をエクスポートすることは避けてください。

型専用の集合にはリテラルユニオンを使用する #

リテラルユニオンは、出力される enum の仕組みなしで JSON 形式の API に適合します。

ts
type Status = "pending" | "running" | "done";

function label(status: Status): string {
  switch (status) {
    case "pending":
      return "Pending";
    case "running":
      return "Running";
    case "done":
      return "Done";
  }
}

制御フロー解析により、各分岐で status が絞り込まれます。ユニオンは、異なるデータを持つバリアントの判別にも使用できます。

ts
type Payment =
  | { kind: "card"; brand: string; lastFour: string }
  | { kind: "cash" }
  | { kind: "wire"; iban: string };

payment.kind === "card" の後では、コンパイラーは brand と lastFour を公開します。ユニオンは、すでに Payment を持っているコードを検査しますが、信頼できない JSON 値を実行時に検証するものではありません。

as const テーブルから型を導出する #

値テーブルを使用すると、実行時検証とコンパイル時のユニオンを 1 つの定義に結び付けられます。

ts
const Status = {
  Pending: "pending",
  Running: "running",
  Done: "done",
} as const satisfies Record<string, string>;

type StatusName = keyof typeof Status;
type Status = (typeof Status)[StatusName];

const statuses: ReadonlySet<string> = new Set(Object.values(Status));

function isStatus(value: unknown): value is Status {
  return typeof value === "string" && statuses.has(value);
}

as const により、プロパティ値が string に拡大されるのを防ぎます。satisfies はリテラル型を破棄せずにテーブルの形状を検査します。読み取り専用のセットが検索のために意図的に任意の文字列を受け入れ、typeof チェックとセットのメンバーシップによって、型述語に必要な実行時の証明を提供します。

順序が主要な要件である場合は、配列を使用します。

ts
const statuses = ["pending", "running", "done"] as const;
type Status = (typeof statuses)[number];

安定した記号キー、メタデータ検索、または既存のメンバーアクセスとの互換性が主要な要件である場合は、オブジェクトを使用します。

検証済みの値をオブジェクトとしてモデル化する #

値オブジェクトは、不変条件と振る舞いが一体となったドメイン値を表します。たとえば、パーセンテージは任意の数値以上のものです。

ts
class Percentage {
  private constructor(readonly value: number) {}

  static from(value: number): Percentage {
    if (!Number.isFinite(value) || value < 0 || value > 100) {
      throw new RangeError("percentage must be finite and between 0 and 100");
    }
    return new Percentage(value);
  }

  asRatio(): number {
    return this.value / 100;
  }

  equals(other: Percentage): boolean {
    return this.value === other.value;
  }

  toJSON(): number {
    return this.value;
  }
}

private コンストラクターにより、呼び出し側は検証を経由することを強制されます。メソッドはドメイン操作に名前を与え、toJSON は境界表現を定義します。JSON の解析結果は数値でありクラスインスタンスではないため、デシリアライゼーションでは再度 Percentage.from を呼び出す必要があります。

ラベルをラップするだけの目的でクラスを使用しないでください。オブジェクトが不変条件を保護する、振る舞いを提供する、または構造的に類似したプリミティブの混同を防ぐ場合に、割り当てと変換のコードが正当化されます。

シリアライゼーション境界を明示的に設計する #

ワイヤ形式では、TypeScript が出力するオブジェクトの形状に依存せず、安定して文書化された文字列または数値を使用してください。数値 enum の自動インクリメント値は、メンバーの順序変更によって壊れやすくなります。逆方向のマッピングは実装詳細であり、プロトコルではありません。

入力境界では、次の手順を実行します。

  1. 信頼できない表現を解析する。
  2. メンバーシップを検証するか、値オブジェクトを構築する。
  3. 検証済みの型をアプリケーション内部で保持する。
  4. JSON、データベース値、またはメッセージを書き込む際に、意図的に変換する。

コンシューマーが出力された実行時の同一性を必要とし、ツールチェーンがその出力を受け入れる場合は、ネイティブ enum を使用します。型専用の集合にはリテラルユニオン、コードでも実行時値が必要な場合には as const テーブル、バリアント固有のデータには判別可能なユニオン、検証と振る舞いには値オブジェクトを使用します。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)