TypeScriptの Branded Types でドメインを守り、コードの安全性を向上させる

に公開

🚀 そのid、本当に安全ですか?

こんにちは!TypeScriptで開発をしている皆さん。
突然ですが、こんな経験はありませんか?

  • 「このid: stringって、何のIDだっけ…? userId? それともorderId?」
  • レビューで「あ、ここの関数に、間違えてproductIdを渡しちゃってますね…」と指摘された。
  • userIdcompanyId、どちらもstring型だから、間違えて代入してもコンパイラが教えてくれず、実行時エラーに…。

TypeScriptの基本的な型(string, numberなど)は非常に便利ですが、
時として、こうした「型の意図が曖昧」なことによるバグを生み出す原因になります。

この記事では、そんな「なんとなくstring型」から卒業するための挑戦として、
Branded Types というテクニックをご紹介します。

Branded Typesとは、TypeScriptの型システムを少し工夫するだけで、
userId型の変数にorderIdを代入する」といった間違いをコンパイル時に検知できるようにする、非常に強力な手法です。

この記事を読み終える頃には、あなたのコードは今よりもずっと安全で、意図が明確になっているはずです。
さあ、一緒にコードの安全性を飛躍させる挑戦を始めましょう!

この記事の対象読者

  • TypeScriptの基本的な型を理解している方
  • より堅牢でバグの少ないコードを書きたいと思っている方
  • anyをなくした次のステップ」を探している方

🧐 問題提起:string型に潜む「見えないリスク」

まずは、具体的なコードで問題点を見てみましょう。
ここにユーザー情報を処理する関数と、注文を処理する関数があるとします。

bad_example.ts
// ユーザー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)の情報を取得しました。"
// コンパイルエラーは起きない。怖い…

UserIdOrderIdtypeエイリアスを使っていますが、中身はどちらもstringです。そのため、TypeScriptコンパイラはgetUser関数にmyOrderIdを渡しても「どちらもstringだからOK!」と判断してしまい、エラーを出してくれません。

これは 「プリミティブ型への執着 (Primitive Obsession)」 と呼ばれるアンチパターンの一種です。
ドメイン(ビジネス領域)における明確な意味を持つ値(ユーザーID、注文IDなど)を、
stringnumberといった汎用的なプリミティブ型で表現してしまうことで、コードの可読性や安全性が低下してしまうのです。

✨ 挑戦:Branded Typesで「ドメイン固有の型」を作る!

この問題を解決するのが Branded Types です。
日本語に訳すなら「印付きの型」といったところでしょうか。

これは、stringnumberといった基本的な型に、幽霊のような「印(ブランド)」を付けることで、名目上は異なる型としてコンパイラに認識させるテクニックです。

ステップ1:Brand型を定義する

まず、このテクニックの核となるBrand型を定義します。
少し不思議な形をしていますが、おまじないだと思って書いてみてください。

type Brand<K, T> = K & { readonly __brand: T };
  • Kには、ベースとなる型(stringnumber)が入ります。
  • Tには、ブランド名となる一意な文字列リテラル(例: "UserId")が入ります。
  • { readonly __brand: T } というユニークなプロパティを交差型(&)で組み合わせることで、TypeScriptに「これはただのK型じゃないぞ」と認識させています。__brandプロパティは実際には存在しない「幽霊プロパティ」です。

ステップ2:ドメイン固有の型を定義する

次に、このBrand型を使って、先ほどのUserIdOrderIdを再定義します。

type Brand<K, T> = K & { readonly __brand: T };

// string型に "UserId" というブランドを付けた型
type UserId = Brand<string, "UserId">;

// string型に "OrderId" というブランドを付けた型
type OrderId = Brand<string, "OrderId">;

これで、UserIdOrderIdは、コンパイラから見て全く異なる型として扱われるようになりました!

ステップ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); // 🚨 コンパイルエラー!

見事にコンパイルエラーが発生しました!
これで、うっかりuserIdorderIdを間違えるというヒューマンエラーを、プログラムの実行前に完全に防ぐことができます。

また、ファクトリ関数(asUserIdなど)の中で、IDのフォーマットチェックのような処理をカプセル化することもできます。

✅ まとめ:TypeScriptで、もう一段階上の安全な世界へ

今回は、「なんとなくstring型」が抱えるリスクを乗り越えるための挑戦として、
Branded Typesを紹介しました。

このテクニックは、ライブラリを追加することなく、TypeScriptの型システムの力だけで実現できます。それでいて、得られるメリットは絶大です。

あなたの身の回りにあるstringnumberで表現されたIDや重要な値。
それらは本当にただのstringで良いのでしょうか?

ぜひ、この記事をきっかけに「ドメイン固有の型」を作ることに挑戦し、
より安全で堅牢なTypeScriptライフを楽しんでください!

Discussion