【入門編】 Branded Types (Nominal Typingの模倣) – TypeScript実践ガイド

TypeScriptの型システム、本当に強力で便利ですよね。「お、コンパイラがバグを未然に防いでくれた!」と感動する瞬間は、エンジニアにとって最高のご褒美の一つだと思います。

でも、TypeScriptを少し使い慣れてくると、こんな「もやっとする瞬間」に出会いませんか?

「あれ、ユーザーID(`userId`)を入れるべきところに、うっかり商品ID(`productId`)を渡しちゃったのに……TypeScriptくん、なんでエラーを出してくれないの!?」

そうなんです。TypeScriptは「構造的部分型(こうぞうてきぶぶんがた)」という性質を持っていて、中身が同じ(どちらもただの文字列や数値)なら、「名前が違うだけでしょ? 同じ仲間だよ!」と優しく(そして時に残酷に)スルーしちゃうんです。

今回は、この「うっかりミス」を型システムで完全にハメ殺す(=絶対にコンパイルエラーにして防ぐ)ための、現場のシークレット・テクニック「Branded Types(ブランド型)」について、お話ししていきますね。

大丈夫、難しい概念に見えるかもしれませんが、身近な例えを使えばすぐに「なるほど!」と腑に落ちるはずです。一緒に紐解いていきましょう!

—

1. なぜ「ただの文字列や数値」だと危ないのか?

まずは、私たちが普段やりがちなコードをちょっと覗いてみてください。

type UserId = string;
type ProductId = string;

function processOrder(userId: UserId, productId: ProductId) {
// 注文処理をするよ
console.log(`ユーザー ${userId} が 商品 ${productId} を買いました`);
}

// 実際の呼び出し
const myUserId: UserId = “user_12345”;
const myProductId: ProductId = “prod_98765”;

// うっかり順番を間違えてしまった!
processOrder(myProductId, myUserId);
// 🚨 TypeScriptは何も文句を言いません(ひえっ……)

どうでしょう? `UserId` も `ProductId` も、実態はどちらもただの `string` ですよね。TypeScriptの目から見ると、`myProductId` も `myUserId` も「文字が入った箱」としては全く同じに見えます。だから、関数に渡す順番を間違えても、エラーを出してくれないんです。

これ、小規模なアプリなら笑い話で済みますが、決済システムやユーザー管理画面でこんなバグが混ざったら……想像しただけで冷や汗が出てきますよね。

—

2. 例え話:美術館の「入場スタンプ」で考えてみよう

この問題を解決するのが Branded Types(ブランド型) です。

ちょっと想像してみてください。
ここに「A館の入場券」と「B館のチケット」があります。どちらもただの「ペラペラの紙」だとします。もし形が全く同じなら、係員もうっかり間違えて案内しちゃうかもしれません。

そこで、美術館のスタッフが「A館専用のスタンプ(ブランド)」をペタッと押したとします。
するとどうでしょう? 紙の材質(中身)は同じ紙なのに、スタンプが押してあることで、「これは絶対にA館のチケットだ!」と一目で区別できるようになりますよね。B館の入り口にこれを出しても、「スタンプが違うので入れません!」と門前払いされます。

TypeScriptのBranded Typesも、まさにこれと同じことをやっています。
「中身はただの `string` だい! でも、私には『ユーザーID専用のスタンプ』が押してあるんだぜ!」という目印(ブランド)を無理やりくっつけるテクニックです。

—

3. 実装してみよう:交差型(Intersection Types)の魔法

それでは、実際にコードで「スタンプ」を押してみましょう。
TypeScriptの交差型(`&`)を使って、こんな風に書きます。

// 1. 「ただの文字列」に「架空のスタンプ(目印)」を合体させる型定義
type UserId = string & { readonly __brand: unique symbol };
type ProductId = string & { readonly __brand: unique symbol };

// 2. それぞれの専用ヘルパー関数(スタンプを押す係の人)を作っておく
function createUserId(id: string): UserId {
return id as UserId; // ここで無理やり(asを使って)スタンプを押す!
}

function createProductId(id: string): ProductId {
return id as ProductId; // こちらも同様
}

// — さあ、使ってみましょう —

const myUserId = createUserId(“user_12345”);
const myProductId = createProductId(“prod_98765”);

function processOrder(userId: UserId, productId: ProductId) {
console.log(`処理成功!`);
}

// ✅ 正しい順番:ちゃんとスタンプが一致しているのでOK!
processOrder(myUserId, myProductId);

// ❌ うっかり逆にした場合:
processOrder(myProductId, myUserId);
// 💥 キタ━━━━(゚∀゚)━━━━!!
// 「型 ‘{ readonly __brand: … }’ の引数を … に割り当てることはできません」
// と、TypeScriptが怒って止めてくれます!

すごい! これで、中身が同じ文字列であっても、スタンプの種類が違うだけでTypeScriptが完璧に区別して、間違った代入や引数の渡しをバシッと弾いてくれるようになりました。

ちなみに、`{ readonly __brand: unique symbol }` という部分は、実際にそんなプロパティが実行時のオブジェクトに存在するわけではありません。「型チェックのためだけに存在する、目に見えない幻のスタンプ」だと思ってください。JavaScriptのランタイム(実行時)のパフォーマンスには一切影響を与えないのでご安心を。

—

4. 実務で使うときのちょっとしたコツ

「毎回 `createUserId` みたいに関数を通すの、ちょっと面倒だな……」と思いましたか?
はい、最初は少しボイラープレート(お決まりのコード)が増えたように感じるかもしれません。

でも、Web開発において「外部から入ってきたデータ(APIのレスポンスや、フォームの入力値)を、アプリ内で安全な型に格上げする(バリデーションを通す)」という作業は、堅牢なアプリを作る上で絶対に避けて通れない道です。

実務では、次のように「型ガード(Type Guard)」や「バリデーション関数」とセットで運用するのが黄金パターンです。

// 例えば、APIから飛んできた文字列が本当にUserIdの形式をしているかチェックする関数
nikov function isValidUserId(value: string): value is UserId {
// ここに正規表現などのチェックを書く
return value.startsWith(“user_”);
}

const inputString = “user_99999”;

if (isValidUserId(inputString)) {
// この中に入った瞬間、inputStringは自動的に安全な「UserId」型に昇格する!
processOrder(inputString, someProductId);
}

こうすることで、「とりあえず何が入ってくるか分からない不安な文字列」から、「アプリ内で安全に扱えることが保証されたID」への境界線がはっきりとコード上に現れ、バグの入り込む隙間がなくなっていきます。

—

まとめ

今回は、構造的部分型をハックして型安全性を極限まで高める「Branded Types」について解説しました。

  • TypeScriptは構造が同じなら同じものとみなしてしまう(うっかりミスが起きやすい)
  • そこに「__brand」という見えないスタンプを交差型(`&`)でくっつけることで、別物として区別させる
  • IDの渡し間違いなどの初歩的かつ致命的なバグを、コンパイル時に100%防げるようになる

最初は「おまじない」のように感じるかもしれませんが、一度この設計の心地よさを知ると、もう普通の `type UserId = string;` には戻れなくなる中毒性があります(笑)。

大規模なWebアプリケーション開発や、チームでの開発で「うっかりミス」を減らしたいときは、ぜひこのBranded Typesを思い出して、プロジェクトに取り入れてみてくださいね。

あなたのTypeScriptライフが、より安心で快適なものになりますように!

コメント

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