TypeScriptの Branded Types でドメインを守り、コードの安全性を向上させる
id
、本当に安全ですか?
🚀 そのこんにちは!TypeScriptで開発をしている皆さん。
突然ですが、こんな経験はありませんか?
- 「この
id: string
って、何のIDだっけ…?userId
? それともorderId
?」 - レビューで「あ、ここの関数に、間違えて
productId
を渡しちゃってますね…」と指摘された。 -
userId
とcompanyId
、どちらもstring
型だから、間違えて代入してもコンパイラが教えてくれず、実行時エラーに…。
TypeScriptの基本的な型(string
, number
など)は非常に便利ですが、
時として、こうした「型の意図が曖昧」なことによるバグを生み出す原因になります。
この記事では、そんな「なんとなくstring
型」から卒業するための挑戦として、
Branded Types というテクニックをご紹介します。
Branded Typesとは、TypeScriptの型システムを少し工夫するだけで、
「userId
型の変数にorderId
を代入する」といった間違いをコンパイル時に検知できるようにする、非常に強力な手法です。
この記事を読み終える頃には、あなたのコードは今よりもずっと安全で、意図が明確になっているはずです。
さあ、一緒にコードの安全性を飛躍させる挑戦を始めましょう!
この記事の対象読者
- TypeScriptの基本的な型を理解している方
- より堅牢でバグの少ないコードを書きたいと思っている方
- 「
any
をなくした次のステップ」を探している方
string
型に潜む「見えないリスク」
🧐 問題提起:まずは、具体的なコードで問題点を見てみましょう。
ここにユーザー情報を処理する関数と、注文を処理する関数があるとします。
// ユーザーIDも注文IDも、ただの string
type UserId = string;
type OrderId = string;
function getUser(userId: UserId): void {
console.log(`ユーザー(ID: ${userId})の情報を取得しました。`);
}
function cancelOrder(orderId: OrderId): void {
console.log(`注文(ID: ${orderId})をキャンセルしました。`);
}
// --- 関数の呼び出し ---
const myUserId: UserId = "user-abc-123";
const myOrderId: OrderId = "order-xyz-789";
// 本来はユーザー情報を取得したいのに…
// あっ!間違えて注文IDを渡してしまった!
getUser(myOrderId);
// => "ユーザー(ID: order-xyz-789)の情報を取得しました。"
// コンパイルエラーは起きない。怖い…
UserId
とOrderId
はtype
エイリアスを使っていますが、中身はどちらもstring
です。そのため、TypeScriptコンパイラはgetUser
関数にmyOrderId
を渡しても「どちらもstring
だからOK!」と判断してしまい、エラーを出してくれません。
これは 「プリミティブ型への執着 (Primitive Obsession)」 と呼ばれるアンチパターンの一種です。
ドメイン(ビジネス領域)における明確な意味を持つ値(ユーザーID、注文IDなど)を、
string
やnumber
といった汎用的なプリミティブ型で表現してしまうことで、コードの可読性や安全性が低下してしまうのです。
✨ 挑戦:Branded Typesで「ドメイン固有の型」を作る!
この問題を解決するのが Branded Types です。
日本語に訳すなら「印付きの型」といったところでしょうか。
これは、string
やnumber
といった基本的な型に、幽霊のような「印(ブランド)」を付けることで、名目上は異なる型としてコンパイラに認識させるテクニックです。
ステップ1:Brand型を定義する
まず、このテクニックの核となるBrand型を定義します。
少し不思議な形をしていますが、おまじないだと思って書いてみてください。
type Brand<K, T> = K & { readonly __brand: T };
-
K
には、ベースとなる型(string
やnumber
)が入ります。 -
T
には、ブランド名となる一意な文字列リテラル(例:"UserId"
)が入ります。 -
{ readonly __brand: T }
というユニークなプロパティを交差型(&
)で組み合わせることで、TypeScriptに「これはただのK
型じゃないぞ」と認識させています。__brand
プロパティは実際には存在しない「幽霊プロパティ」です。
ステップ2:ドメイン固有の型を定義する
次に、このBrand
型を使って、先ほどのUserId
とOrderId
を再定義します。
type Brand<K, T> = K & { readonly __brand: T };
// string型に "UserId" というブランドを付けた型
type UserId = Brand<string, "UserId">;
// string型に "OrderId" というブランドを付けた型
type OrderId = Brand<string, "OrderId">;
これで、UserId
とOrderId
は、コンパイラから見て全く異なる型として扱われるようになりました!
ステップ3:安全な型の生成と利用
しかし、このままではUserId
型の値を作ることができません。
// このコードはコンパイルエラーになる!
// 型 'string' を型 'UserId' に割り当てることはできません。
// 型 'string' を型 '{ readonly __brand: "UserId"; }' に割り当てることはできません。
const userId: UserId = "user-abc-123";
"user-abc-123"
というただのstring
には、__brand: "UserId"
というプロパティがないためです。
これは意図した動作で、どこでも好き勝手にUserId
を作れないようにする、いわば安全装置です。
ではどうすればよいか?
型を生成するための専用の関数(ファクトリ関数) を用意するのが定石です。
// --- 型定義 ---
type Brand<K, T> = K & { readonly __brand: T };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
// --- ファクトリ関数 ---
// UserIdを安全に生成する関数
function asUserId(id: string): UserId {
return id as UserId; // 型アサーションで「これはUserIdです」と宣言
}
// OrderIdを安全に生成する関数
function asOrderId(id: string): OrderId {
return id as OrderId;
}
// --- 改良版の関数 ---
function getUser(userId: UserId): void {
console.log(`ユーザー(ID: ${userId})の情報を取得しました。`);
}
function cancelOrder(orderId: OrderId): void {
console.log(`注文(ID: ${orderId})をキャンセルしました。`);
}
// --- 安全な呼び出し ---
const myUserId = asUserId("user-abc-123");
const myOrderId = asOrderId("order-xyz-789");
getUser(myUserId); // OK!
// やった!コンパイルエラーで守られた!
// 型 'OrderId' の引数を型 'UserId' のパラメーターに割り当てることはできません。
// 型 'OrderId' を型 '{ readonly __brand: "UserId"; }' に割り当てることはできません。プロパティ '__brand' の型に互換性がありません。
// 型 '"OrderId"' を型 '"UserId"' に割り当てることはできません。
getUser(myOrderId); // 🚨 コンパイルエラー!
見事にコンパイルエラーが発生しました!
これで、うっかりuserId
とorderId
を間違えるというヒューマンエラーを、プログラムの実行前に完全に防ぐことができます。
また、ファクトリ関数(asUserId
など)の中で、IDのフォーマットチェックのような処理をカプセル化することもできます。
✅ まとめ:TypeScriptで、もう一段階上の安全な世界へ
今回は、「なんとなくstring
型」が抱えるリスクを乗り越えるための挑戦として、
Branded Typesを紹介しました。
このテクニックは、ライブラリを追加することなく、TypeScriptの型システムの力だけで実現できます。それでいて、得られるメリットは絶大です。
あなたの身の回りにあるstring
やnumber
で表現されたIDや重要な値。
それらは本当にただのstring
で良いのでしょうか?
ぜひ、この記事をきっかけに「ドメイン固有の型」を作ることに挑戦し、
より安全で堅牢なTypeScriptライフを楽しんでください!
Discussion