Interactive

ブランド型と幽霊型

ブランド型と幽霊型は、どちらも実行時の値を増やさずに型レベルの区別を加えられますが、用途は異なります。

ブランド型

ブランド型は、構造が同じ値を名目的に区別する手法です。stringで表した利用者IDと請求ID、numberで表した円と割合のように、取り違えが重大な値へ使います。

import { z } from "zod"

const UserIdSchema = z.string().uuid().brand<"UserId">()
type UserId = z.infer<typeof UserIdSchema>

const userId = UserIdSchema.parse(input)

生成は検証関数やスキーマに集約し、呼び出し側で直接as UserIdとしません。ブランドはコンパイル時の区別であり、JSON、データベース、外部APIから受け取った値には残らないため、境界で再検証します。

幽霊型

幽霊型(Phantom Type)は、実行時表現には使わない型パラメータで操作の可否を区別する手法です。検証前後や接続前後など、同じデータを持ちながら許される操作だけが変わる場合に使います。こうした状態別APIはTypestateとも呼ばれます。

declare const phase: unique symbol

type Document<Phase> = {
  readonly content: string
  readonly [phase]: Phase
}

declare function submit(
  document: Document<"draft">,
): Document<"submitted">

TypeScriptは構造的型付けを行うため、型パラメータが型の構造へ一度も現れないContainer<T>は、Tが違っても区別されません。unique symbolのプロパティや、クラスの型専用フィールドで型パラメータを構造へ反映します。

クラスで型専用フィールドを宣言する場合、!は「初期化済み」とコンパイラへ伝えるだけで、フィールドを消去する指定ではありません。実行時へ出力しない意図ならdeclareを使い、コンパイル結果も確認します。

採用しない場面

  • 外部入力の妥当性を保証したいだけなら、実行時スキーマを使う
  • 現在の状態を保存、表示、監査する必要があるなら、実データにも状態を持たせる
  • 権限や不正な状態遷移をサーバーで拒否する必要があるなら、型だけに任せない
  • 生成箇所を管理できず、アサーションが各所へ広がるなら導入しない

参考資料