【実務・中級編】プリミティブ型のサブタイプ化:リテラル型で実現するドメイン駆動設計の第一歩 – TypeScript コア・型システムの基礎解析バイブル

コードレビューをしていて、次のようなコードに遭遇したことはないだろうか。

// よくある「すべてを string で解決してしまう」アンチパターン
function cancelOrder(orderId: string, status: string): void {
// …
}

// 呼び出し側
cancelOrder(“12345”, “active”); // 意図しないステータスやIDの順序ミスが起きても、型システムは何も言わない

フロントエンド開発、コンポーネント設計、そしてAPI連携。あらゆるレイヤーで、私たちは `string` や `number` という「広すぎるプリミティブ型」の海に溺れている。`orderId` の代わりに `userId` が渡されても、ステータスに `”activ”` というタイポがあっても、TypeScriptのコンパイラは平然とビルドを通してしまう。

これは型安全ではない。単なる「アノテーション付きのJavaScript」だ。

今回は、TypeScriptの型システムの本質である「サブタイプとリテラル型」を極限まで利用し、コンパイル時にドメインの矛盾を完全に排除するプロダクション設計術を伝授する。

—

1. プリミティブ型とリテラル型の決定的な違い

TypeScriptにおいて、プリミティブ型(`string`, `number` など)は「無限の可能性を持つ集合」である。一方で、リテラル型(`”active”`, `42` など)は、その集合の真部分集合(サブタイプ)である。

type BaseString = string; // すべての文字列の集合
type ActiveStatus = “active”; // “active” という「たった1つの値」しか持たない集合

let a: BaseString = “hello”;
let b: ActiveStatus = “active”;

a = b; // OK: “active” は string のサブタイプだから代入できる
b = a; // Error: string は “active” のサブタイプではない!

この「代入可能性の非対称性」こそが、ドメイン駆動設計(DDD)をTypeScriptで実装する際の最強の武器となる。ドメインの概念(ID、ステータス)をプリミティブの海から引き剥がし、適切なリテラル型・ブランド型として閉じ込めることで、コンパイラを最強のガードマンに変えるのだ。

—

2. 実践:ドメイン駆動型ステータス管理と型安全ID

実務の現場で即座に使える、堅牢なプロダクションコードの設計パターンを見ていこう。ここでは「Eコマースの注文ドメイン」を題材にする。

プレーンなstringを駆逐する「ブランデッド型(Nominal Typing)」

単なる `string` の別名(Type Alias)では、構造的型付け(Structural Typing)により、異なるID同士の混同を防げない。そこで、Branded Types(名目型風のハック)を用いる。

// — 1. ブランデッド型の基礎定義 —
declare const __brand: unique symbol;
type Brand = T & { readonly [__brand]: B };

// ドメイン固有のID型
export type OrderId = Brand;
export type CustomerId = Brand;

// ファクトリー関数(実行時のバリデーションと型の昇格を同時に行う)
export function createOrderId(id: string): OrderId {
if (!id.startsWith(“ord_”)) {
throw new Error(`Invalid OrderId format: ${id}`);
}
return id as OrderId;
}

export function createCustomerId(id: string): CustomerId {
if (!id.startsWith(“cust_”)) {
throw new Error(`Invalid CustomerId format: ${id}`);
}
return id as CustomerId;
}

この設計により、`OrderId` が期待される関数に `CustomerId` を渡すと、コンパイルエラーが発生する。文字列という「中身が同じプリミティブ」であっても、ドメインの文脈が違う値の混入をコンパイラが完全にシャットアウトする。

2. ステータス遷移のマトリクスを型で縛る

次に、ステータス管理だ。単なる `string` や、緩い `type Status = “pending” | “paid” | “shipped”` では不十分だ。「どのステータスからどのステータスへ遷移できるか」というビジネスルールを型レベルで強制する。

// ステータスの定義
export const OrderStatus = {
PENDING: “PENDING”,
PAID: “PAID”,
SHIPPED: “SHIPPED”,
CANCELLED: “CANCELLED”,
} as const;

export type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];

// 許容される状態遷移のマトリクスを型として定義
type StateTransitionMap = {
[OrderStatus.PENDING]: OrderStatus.PAID | OrderStatus.CANCELLED;
[OrderStatus.PAID]: OrderStatus.SHIPPED | OrderStatus.CANCELLED;
[OrderStatus.SHIPPED]: never; // 発送済からはどこにも遷移できない
[OrderStatus.CANCELLED]: never; // キャンセル済からもどこにも遷移できない
};

// 型安全なステータス変更関数
export function transitionOrder(
currentStatus: T,
nextStatus: StateTransitionMap[T]
): StateTransitionMap[T] {
// 実行時ロジック…
console.log(`Transitioning from ${currentStatus} to ${nextStatus}`);
return nextStatus;
}

// — 使用例 —
let current = OrderStatus.PENDING;

current = transitionOrder(current, OrderStatus.PAID); // OK: PENDING -> PAID は許可されている
// current = transitionOrder(current, OrderStatus.SHIPPED);
// ❌ コンパイルエラー!
// 「Argument of type ‘”SHIPPED”‘ is not assignable to parameter of type ‘PAID | CANCELLED’」

このコードの美しさは、「ビジネスルールの変更が、そのまま型定義の変更に直結し、コンパイラがバグの混入を検知する」という点にある。テストを書かなくても、型がテストの役割の一部を完全に代替してくれるのだ。

—

3. API境界(Network Boundary)における型の安全地帯の作り方

フロントエンド開発において最大の難所は、「信頼できない外部(バックエンドAPIや外部JSON)」から送られてくるデータを、いかにして安全なドメインモデルに変換するかである。

ここで `as OrderId` や `as any` といった「型の嘘」をついてはならない。境界線では必ずランタイムバリデーションを行い、そこで初めてプリミティブからリテラル型(ブランド型)へ昇格させる。

import { z } from “zod”; // ランタイムバリデーションライブラリの代表例

// Zodスキーマによるランタイム定義
const OrderSchema = z.object({
id: z.string().transform((val) => createOrderId(val)),
customerId: z.string().transform((val) => createCustomerId(val)),
status: z.nativeEnum(OrderStatus),
});

type RawOrderResponse = z.infer;

// APIクライアント関数
export async function fetchOrder(endpoint: string): Promise {
const response = await fetch(endpoint);
const json = await response.json();

// 実行時パース。ここで構造と値が検証され、失敗すれば例外を投げる
const result = OrderSchema.parse(json);

return result.id; // ここで確実に安全な OrderId 型として返却される
}

ネットワーク境界という「不確実性のフロンティア」を抜けた瞬間から、アプリケーション内は完全に純粋な型安全の世界へと突入する。これが、モダンWebフロントエンドにおける最高峰のアーキテクチャである。

—

チーフアーキテクトからの提言

プリミティブ型をそのままコードのあちこちに露出させる設計は、技術的負債の複利を発生させる。
「たかが文字列」「たかが数値」と侮ってはいけない。そのプリミティブに適切な名前を与え、リテラル型やブランド型でコンテキストを付与すること。

型とは、単なる補完のためのツールではない。「あなたのコードが絶対に犯してはならない過ちを証明するための数学的証明システム」なのだ。

明日のコードレビューでは、`string` や `number` が裸のままビジネスロジックの引数に使われていないか、厳しく目を光らせてほしい。あなたのプロダクトは、もっと堅牢になれるはずだ。

タイトルとURLをコピーしました