第36章:ブランド型(ID取り違え防止)🏷️

🎯目的
「string だから入れ替えても通っちゃう…😱」を コンパイルで止める ようにするよ!
UserId と ProductId を 別の型として扱える ようにして、取り違え事故を消す🔥
📚学ぶこと
-
🧠 TypeScriptは「構造的型付け」なので、同じ
stringは基本同じ扱いになりがち -
🏷️ **ブランド型(Branded / Opaque / Nominalっぽくする)**で「意味」を型に乗せる
-
🛡️ 2つの作り方
- ① かんたん版:
__brandを足す(学習しやすい) - ② 強つよ版:
unique symbolを使う(衝突しにくい) (DEV Community)
- ① かんたん版:
-
🧪 「型のテスト」=動かすテストじゃなくて、コンパイルで保証する(Vitestでできる) (Vitest)
※ちなみに現時点の最新安定版TypeScriptは GitHub Releases 上で TypeScript 5.9.3 が “Latest” になってるよ🆕 (GitHub)
🧪手を動かす(ミニ題材:カートに追加🛒)
「ユーザーID」と「商品ID」を取り違えるとヤバい、を型で止めるよ💥
1) まず“事故るコード”を用意する😵💫
✅ 事故の構図
addToCart(userId: string, productId: string)→ 引数を入れ替えてもコンパイルが通る💀
// src/cart.ts
export function addToCart(userId: string, productId: string) {
return { userId, productId }
}
// どっちも string だから、入れ替えても通っちゃう…
addToCart("prd_001", "usr_001") // 😱
ここから「入れ替えたらコンパイルで落ちる」に進化させるよ💪
🧪🟥→🟩→🧹(TDDっぽく進めるよ)🚦✨
ポイント:今回は “型の失敗=Red” だよ!
Vitest は *.test-d.ts を 型テストとして扱えるよ🧪(実行はしないで、コンパイルチェックだけする) (Vitest)
2) 🟥 Red:型テストで「入れ替えを禁止したい」を書く✍️
@ts-expect-error を使うと、**「ここはエラーになるのが正しい」**が書けるよ🙆♀️
(エラーにならなかったら、@ts-expect-error が “無駄” って扱いになってテストが落ちる=Red!)
// src/ids.test-d.ts
import { addToCart2, UserId, ProductId } from "./ids.js"
// 正しい呼び出しはOKのはず
addToCart2(UserId("usr_001"), ProductId("prd_001"))
// 入れ替えたらコンパイルで落ちてほしい!
// @ts-expect-error - userId と productId を取り違えたらダメ🙅♀️
addToCart2(ProductId("prd_001"), UserId("usr_001"))
この時点では UserId / ProductId / addToCart2 が無いので当然落ちるね🟥😆
(もしくは “ただの string なら入れ替えても通る” ので @ts-expect-error が無効扱いになって落ちる🟥)
3) 🟩 Green:ブランド型を作る(かんたん版)🏷️
まずは一番わかりやすい版からいくね☺️
「string だけど、UserId という印がついた string」みたいにする✨
// src/ids.ts
type Brand<T, Name extends string> = T & { readonly __brand: Name }
export type UserId = Brand<string, "UserId">
export type ProductId = Brand<string, "ProductId">
// 💡 生成関数で “as” を隠す(アプリ側に撒かないのがコツ!)
export function UserId(value: string): UserId {
// ここは軽いチェックでもOK(好みで強化してね)
if (!value.startsWith("usr_")) throw new Error("UserId must start with usr_")
return value as UserId
}
export function ProductId(value: string): ProductId {
if (!value.startsWith("prd_")) throw new Error("ProductId must start with prd_")
return value as ProductId
}
export function addToCart2(userId: UserId, productId: ProductId) {
return { userId, productId }
}
これで ids.test-d.ts の
addToCart2(ProductId(...), UserId(...)) が 型エラーになって、@ts-expect-error も満たせる=🟩になるよ🎉
ブランド型は「意味が違うのに同じ型に見える」を分離する定番パターンだよ🧠 (Total TypeScript)
4) 🧹 Refactor:強つよ版(unique symbol)も知っておく💪✨
チームや規模が大きいと「同名ブランド」衝突が怖いことがあるのね🫠
その対策として unique symbol をキーにするやり方があるよ(より衝突しにくい) (DEV Community)
// src/ids-unique.ts
declare const userIdBrand: unique symbol
declare const productIdBrand: unique symbol
export type UserId = string & { readonly [userIdBrand]: "UserId" }
export type ProductId = string & { readonly [productIdBrand]: "ProductId" }
export function UserId(value: string): UserId {
if (!value.startsWith("usr_")) throw new Error("UserId must start with usr_")
return value as UserId
}
export function ProductId(value: string): ProductId {
if (!value.startsWith("prd_")) throw new Error("ProductId must start with prd_")
return value as ProductId
}
どっちを使えばいい?🤔
- 👶 学習&アプリ開発:
__brand版でぜんぜんOK🙆♀️ - 🏢 ライブラリ化 / 超巨大:
unique symbol版が安心寄り🛡️ (DEV Community)
🧪 型テストをVitestで回す(サクッと)🔁
Vitestは 型テストを公式にサポートしてるよ🧪
*.test-d.ts は「実行しないで型チェックだけ」って扱いになる✨ (Vitest)
expectTypeOf みたいな型アサーションも使えるよ🧠 (Vitest)
「Vitestじゃなくて型テスト専用が良い」なら tsd みたいな選択肢もあるよ🧰 (GitHub)
🤖AIの使いどころ(コピペ用)🤖✨
① ブランド化の設計レビュー🧑⚖️
UserId と ProductId をブランド型にしたいです。
「as をアプリ側に撒かない」前提で、生成関数の設計案と注意点を3つ出して。
② “取り違え事故”の洗い出し🔍
このコードで「同じ型に見えるけど意味が違う値」を列挙して。
ID/金額/日付/メール等の取り違え事故になりそうな箇所を指摘して。
③ 型テストのネタ出し🧪
ブランド型にしたので、@ts-expect-error を使った「間違い呼び出し」テスト例を5個作って。
✅チェックリスト(合格ライン)💮
- ✅
addToCart2(UserId, ProductId)は通る - ✅
addToCart2(ProductId, UserId)は 型で落ちる(=事故が消える) - ✅
as UserIdをアプリのあちこちに書いてない(生成関数に封じ込めた) - ✅ ブランド型を導入したことで「引数の意味」が読みやすくなった📖✨
☠️よくある落とし穴(ここだけ注意!)⚠️
- 😈
as UserIdをどこでも使い始める → 型安全が崩壊するので、生成関数へ隔離しよ! - 🫠 IDの形式チェックをゼロにすると「なんでもUserId」になりがち
→ 最低限
usr_/prd_だけでも守ると事故が激減するよ👍 - 🧩 似た概念もブランド分け推奨:
Email/Url/Money/ISODateStringなど✨
🎀まとめ
ブランド型は「string地獄😵💫」を抜ける最初の一歩🏃♀️💨 テスト(型テスト)にすると、**“取り違えが起きない設計”**がずっと維持できるよ🧪💕
次(第37章)チラ見せ👀✨
次は Result型 で「失敗を仕様にする」🧯 例外を乱用しないで、呼び出し側も含めて安全にできるようになるよ〜!