こんにちは!TypeScriptの世界へようこそ。
日々、フロントエンドからバックエンドまで設計していると、「たかが文字列、されど文字列」のバグに頭を悩ませることが本当によくありますよね。
他のオブジェクト指向言語(JavaやC#など)からやってきた開発者の方によくあるのが、次のようなコードです。
function cancelOrder(orderId: string, status: string) {
// 実装…
}
// 呼び出し側
cancelOrder(“processing”, “ORD-12345”); // あれ? 引数の順番が逆だ……!
驚くべきことに、このコードはTypeScriptのコンパイラを完璧にすり抜けてエラーになりません。なぜなら、`”processing”`も`”ORD-12345″`も、どちらもただの`string`型だからです。
「文字列なんだから、どこに何を渡しても型としては一緒だよ」——TypeScriptがそう判断してしまうため、うっかり引数を逆にしてバグを生んでしまっても、実行するまで気づけない。これでは、せっかくTypeScriptを使っている意味が半減してしまいますよね。
ここをクリアすれば、あなたの書くコードの安全性は劇的に跳ね上がります。今回は、TypeScriptの「リテラル型」を駆使して、コンパイラの力を味方につけるドメイン駆動設計(DDD)の第一歩を一緒にマスターしていきましょう!
—
1. プリミティブ型と「リテラル型」の決定的な違い
まずは、型システムが内部でどう見ているのかを解きほぐしていきますね。
通常の `string` や `number` は、「広大な海」のようなものです。どんな文字列でも、どんな数字でも受け入れてしまいます。
一方、リテラル型(Literal Type)とは、その海のなかにある「たった一滴の水滴(特定の値そのもの)」を指します。
// 1. プリミティブ型(何でも入る海)
let generalString: string = “hello”;
generalString = “world”; // OK!何でも入れられます
// 2. リテラル型(その値だけを許す厳格な枠)
let specificString: “hello” = “hello”;
// specificString = “world”;
// ❌ エラー: Type ‘”world”‘ is not assignable to type ‘”hello”‘.
「たった一つの値しか入らない変数なんて、何に使うの?」って思いますよね。
ここに、複数のリテラル型を「`|`(ユニオン型)」で組み合わせるという魔法を使うと、真価を発揮します。
type OrderStatus = “pending” | “processing” | “shipped” | “cancelled”;
let currentStatus: OrderStatus = “processing”; // OK!
// currentStatus = “unknown_status”;
// ❌ エラー!定義されていない文字列は一発でコンパイルエラーになります
この「決められた値しか受け付けない」という性質こそが、ビジネスロジックの誤りを防ぐ強力な盾になります。
—
2. ドメイン駆動設計(DDD)の第一歩:プリミティブ執着からの脱却
ドメイン駆動設計の世界では、意味を持たないプリミティブ型(`string`や`number`など)をそのままドメインロジックで使い回すアンチパターンを「プリミティブ執着(Primitive Obsession)」と呼びます。
例えば、ECサイトのコードで考えてみましょう。
「ユーザーID」と「商品ID」は、どちらもデータベース上は文字列(`string`)かもしれません。しかし、ユーザーIDの代わりに商品IDを渡してしまったら大問題ですよね。
これをTypeScriptのリテラル型(およびブランド型と呼ばれる発展形の手前)を使って、型で厳密に区別してみましょう。
/
- ユーザーを一意に識別するID型
- ただのstringではなく、”UserIdという文脈を持ったstring”として定義します
/
type UserId = string & { readonly __brand: unique symbol };
/
- 注文を一意に識別するID型
/
type OrderId = string & { readonly __brand: unique symbol };
// ファクトリー関数(安全にIDを生成するヘルパー)
function createUserId(id: string): UserId {
return id as UserId;
}
function createOrderId(id: string): OrderId {
return id as OrderId;
}
// 実際のビジネスロジック関数
function getOrderHistory(userId: UserId, orderId: OrderId) {
console.log(`Fetching order ${orderId} for user ${userId}`);
}
// — 使用例 —
const userId = createUserId(“usr_999”);
const orderId = createOrderId(“ord_888”);
// ✅ 正しい呼び出し
getOrderHistory(userId, orderId);
// ❌ 誤って引数を逆にしてしまった場合
// getOrderHistory(orderId, userId);
// コンパイルエラー!
// Argument of type ‘OrderId’ is not assignable to parameter of type ‘UserId’.
おっと、少し高度なテクニック(ブランド型)が混ざってしまいましたか?
でも安心してください。初学者のうちは、次のような文字列リテラル型の組み合わせから始めるだけで、十分すぎるほどの効果を実感できます。
// ステータスや役割(ロール)をリテラル型で表現するだけでも、バグの9割は防げます
type UserRole = “admin” | “editor” | “viewer”;
type PaymentMethod = “credit_card” | “bank_transfer” | “apple_pay”;
function processPayment(method: PaymentMethod, amount: number) {
// 存在しない決済手段を指定すると、即座にエディタが赤く波線を引いて教えてくれます
}
—
3. 現場でやりがちな「文法エラー」と陥りやすい罠
ここで、リテラル型を使い始めるときに誰もがハマる「代表的な罠」を2つご紹介しておきますね。
罠1: `const` で宣言し忘れて、ただの `string` に戻ってしまう現象
TypeScriptの型推論は非常に賢いですが、時々「親切心」が裏目に出ます。
// やりがちなミス
let status = “processing”;
// 型推論はどうなるか?
// let status: string <- リテラル型ではなく、広範囲なstring型になってしまう!
type OrderStatus = "pending" | "processing";
function updateStatus(s: OrderStatus) {}
// updateStatus(status);
// ❌ エラー! "string" は "pending" | "processing" に代入できません
【解決策】
値が再代入されないのであれば、`let` ではなく `const` を使いましょう。`const` で宣言された変数は、TypeScriptによって自動的にリテラル型として推論されます。
const status = “processing”; // 型は “processing”
updateStatus(status); // OK!
罠2: オブジェクトのプロパティが書き換え可能になってしまう現象
APIから取得した設定値などをオブジェクトとして持たせる場合も注意が必要です。
const config = {
mode: “development”, // 型は string と推論されがち
};
function setMode(m: “development” | “production”) {}
// setMode(config.mode);
// ❌ エラー! config.mode は string型 なので代入できません
【解決策】
オブジェクト全体を「変更不可(イミュータブル)」にする `as const`(constアサーション)を使いましょう。これによって、オブジェクトのプロパティすべてが極限まで狭いリテラル型に固定されます。
const config = {
mode: “development”,
} as const;
// config.mode の型は “development” に固定される!
setMode(config.mode); // ✨ 完璧にコンパイル通ります!
—
まとめ:型は「制約」ではなく、開発者を守る「最強のドキュメント」
今回は、プリミティブ型のサブタイプ化とリテラル型を活用したドメイン駆動設計の第一歩について解説しました。
- `string` や `number` のような広い海ではなく、リテラル型で「値の正確な居場所」を定義する。
- 順番ミスやタイポといった人間がやりがちなケアレスミスを、実行前(コンパイル時)にすべて検知する。
- `const` や `as const` を使って、型推論を思い通りにコントロールする。
TypeScriptの型システムは、コードを縛り付ける窮屈なものではありません。
「未来の自分やチームメンバーが、安心して高速にコードを書くための羅針盤」です。
ここをクリアできれば、あなたのTypeScriptのコードは「動くだけのコード」から「意図が明確で絶対に壊れない頑健なコード」へと進化します。
ぜひ、今日書くコードのステータスやIDの定義から、リテラル型を取り入れてみてくださいね。バグの少ない快適な開発ライフを応援しています!