TypeScript Brand Type:型安全IDと型ブランド実践
前端开发
TypeScript Brand Type:型安全IDと型ブランド実践
TypeScriptは構造的型システムです——形が同じなら型は互換します。つまり、UserIdとOrderIdがどちらもstringの場合、相互に代入でき、コンパイラはエラーを出しません。ビジネスコードではこれは災難です:ユーザーIDを注文照会インターフェースに渡し、実行時にエラーを発見することになります。
Brand Type(型ブランド)は、TypeScriptで名義型(Nominal Typing)を実現する古典的なパターンです。型に一意のマークを「刻印」することで、構造が同じ型でもコンパイル時に互換しなくなります。本記事は基礎から本番級まで、完全なBrand Type実践ガイドを提供します。
コア概念早見表
| 概念 | 構造的型 | Brand Type(名義型) | 差異 |
|---|---|---|---|
| 型互換性 | 構造が同じなら互換 | 構造が同じでも非互換 | ⭐⭐⭐⭐⭐ |
| コンパイル時チェック | 弱い(ID混用を検出不可) | 強い(ID混用を即座に検出) | ⭐⭐⭐⭐⭐ |
| 実行時オーバーヘッド | なし | なし(型消去) | ⭐ |
| コード可読性 | 弱い(すべてstring) | 強い(セマンティクスが明確) | ⭐⭐⭐⭐ |
| リファクタリング安全性 | 低い | 高い(型変更が自動伝播) | ⭐⭐⭐⭐⭐ |
| 学習コスト | 低い | 中程度 | ⭐⭐⭐ |
5つのペインポイント分析
ペインポイント1:プリミティブID型の混用を検出できない
// 構造的型付け下での災難
function getUser(id: string) { /* ... */ }
function getOrder(id: string) { /* ... */ }
const userId = 'user_123'
const orderId = 'order_456'
getUser(orderId) // コンパイル通過!実行時にエラー発見
getOrder(userId) // コンパイル通過!実行時にエラー発見
ペインポイント2:単位型をコンパイル時に区別できない
// 金額単位の混用
function processPayment(amount: number, currency: string) { /* ... */ }
processPayment(100, 'CNY') // 100は分か元か?
processPayment(100, 'USD') // 100はセントかドルか?
ペインポイント3:状態遷移を型レベルで制約できない
// 型制約のない状態機械
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'completed'
function transition(current: OrderStatus, next: OrderStatus) { /* ... */ }
transition('completed', 'pending') // コンパイル通過!しかしビジネス上は許可されない
ペインポイント4:APIレスポンス型にドメインセマンティクスが不足
// APIレスポンスはプリミティブ型、ビジネスセマンティクスが不足
interface ApiResponse {
id: string // 何のID?
parentId: string // 親の何のID?
refId: string // 何への参照ID?
}
ペインポイント5:クロスモジュールの型伝達に安全保証がない
// クロスモジュール伝達時に型情報が消失
function parseInput(raw: string): number { return Number(raw) }
function calculateArea(width: number, height: number): number { /* ... */ }
const userId = parseInput('123') // numberを返すが、セマンティクスはUserId
calculateArea(userId, 100) // コンパイル通過!しかしセマンティクスは完全に間違い
5つのコアパターン実践
パターン1:Brand Type基礎定義
実行環境: TypeScript 5.5+, Node.js 20+
// brand.ts - Brand Typeコア定義
/**
* Brand Typeユーティリティ
* 交差型で基底型に一意のブランドマーカーを追加
* 実行時は完全に消去、ゼロオーバーヘッド
*/
// ===== 1. 基礎Brand定義 =====
/**
* ブランド型を作成する汎用ユーティリティ
* @template T - 基底プリミティブ型
* @template Brand - ブランドマーカー(一意識別子)
*/
type Brand<T, Brand extends string> = T & { readonly __brand: Brand }
// 具体的なブランド型定義
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type ProductId = Brand<string, 'ProductId'>
// 数値型ブランド
type Milliseconds = Brand<number, 'Milliseconds'>
type Seconds = Brand<number, 'Seconds'>
type Pixels = Brand<number, 'Pixels'>
// ===== 2. ブランドコンストラクタ =====
/**
* 型安全なブランド値コンストラクタ
* コンストラクタ経由でのみブランド値を作成可能
*/
function createUserId(value: string): UserId {
return value as UserId
}
function createOrderId(value: string): OrderId {
return value as OrderId
}
function createMilliseconds(value: number): Milliseconds {
return value as Milliseconds
}
// ===== 3. コンパイル時型安全検証 =====
const userId = createUserId('user_123')
const orderId = createOrderId('order_456')
// ✅ 正しい:型が一致
function getUser(id: UserId): void { console.log('User:', id) }
getUser(userId) // OK
// ❌ コンパイルエラー:型が不一致
// getUser(orderId) // Error: Argument of type 'OrderId' is not assignable to parameter of type 'UserId'
// ❌ コンパイルエラー:プリミティブ型を直接代入不可
// const uid: UserId = 'raw_string' // Error: Type 'string' is not assignable to type 'UserId'
// brand-advanced.ts - 高度なBrand Typeパターン
/**
* 不変Brand:ブランド値の変更を防止
*/
type ImmutableBrand<T, Brand extends string> = Readonly<T> & {
readonly __brand: Brand
readonly __immutable: true
}
/**
* 検証付きBrand:構築時に実行時検証を実行
*/
type ValidatedBrand<T, Brand extends string> = T & {
readonly __brand: Brand
readonly __validated: true
}
/**
* 検証付きブランドコンストラクタを作成
*/
function createValidatedBrand<T, Brand extends string>(
brand: Brand,
validate: (value: T) => boolean,
errorMessage: string
): (value: T) => ValidatedBrand<T, Brand> {
return (value: T): ValidatedBrand<T, Brand> => {
if (!validate(value)) {
throw new TypeError(`Validation failed for ${brand}: ${errorMessage}`)
}
return value as ValidatedBrand<T, Brand>
}
}
// 使用例:メールブランド型
type Email = ValidatedBrand<string, 'Email'>
const createEmail = createValidatedBrand(
'Email',
(v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
'Invalid email format'
)
const email = createEmail('tanaka@toolsku.com') // OK
// createEmail('not-an-email') // TypeError: Validation failed for Email: Invalid email format
// 使用例:正の整数ブランド型
type PositiveInt = ValidatedBrand<number, 'PositiveInt'>
const createPositiveInt = createValidatedBrand(
'PositiveInt',
(v: number) => Number.isInteger(v) && v > 0,
'Value must be a positive integer'
)
const count = createPositiveInt(42) // OK
// createPositiveInt(-1) // TypeError
// createPositiveInt(3.14) // TypeError
パターン2:型安全ID
// branded-ids.ts - 本番級型安全IDシステム
/**
* IDブランド型システム
* 解決:異なるエンティティIDの混用、ID形式検証、IDシリアライズ/デシリアライズ
*/
// ===== 1. IDブランド定義 =====
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type ProductId = Brand<string, 'ProductId'>
type TenantId = Brand<string, 'TenantId'>
type SessionId = Brand<string, 'SessionId'>
// UUID形式のIDブランド
type UuidBrand<T extends string> = ValidatedBrand<string, T>
type UserUuid = UuidBrand<'UserUuid'>
type OrderUuid = UuidBrand<'OrderUuid'>
// ===== 2. IDコンストラクタファクトリ =====
interface IdConfig {
prefix: string
separator?: string
validate?: (value: string) => boolean
}
function createIdFactory<T extends string>(config: IdConfig) {
const separator = config.separator ?? '_'
const validate = config.validate ?? ((v: string) => v.startsWith(config.prefix + separator))
function create(raw: string): Brand<string, T> {
if (!validate(raw)) {
throw new TypeError(
`Invalid ID format for ${config.prefix}: expected "${config.prefix}${separator}...", got "${raw}"`
)
}
return raw as Brand<string, T>
}
function generate(): Brand<string, T> {
const id = `${config.prefix}${separator}${crypto.randomUUID().slice(0, 8)}`
return id as Brand<string, T>
}
function isType(value: string): value is Brand<string, T> {
return validate(value)
}
return { create, generate, isType }
}
// ===== 3. 具体的なIDファクトリ =====
const UserIdFactory = createIdFactory<'UserId'>({
prefix: 'usr',
validate: (v) => /^usr_[a-zA-Z0-9]{8,}$/.test(v),
})
const OrderIdFactory = createIdFactory<'OrderId'>({
prefix: 'ord',
validate: (v) => /^ord_[a-zA-Z0-9]{8,}$/.test(v),
})
const ProductIdFactory = createIdFactory<'ProductId'>({
prefix: 'prd',
validate: (v) => /^prd_[a-zA-Z0-9]{8,}$/.test(v),
})
// ===== 4. 使用例 =====
// 新しいIDを生成
const newUserId = UserIdFactory.generate() // "usr_a1b2c3d4"
const newOrderId = OrderIdFactory.generate() // "ord_e5f6g7h8"
// 文字列から作成(検証付き)
const existingUserId = UserIdFactory.create('usr_existing1')
// UserIdFactory.create('ord_wrong_prefix') // TypeError
// 型ガード
function processId(raw: string) {
if (UserIdFactory.isType(raw)) {
// rawはUserIdに絞り込まれる
getUser(raw) // OK
}
}
// ===== 5. API型でのID使用 =====
interface UserDTO {
id: UserId
name: string
email: Email
tenantId: TenantId
}
interface OrderDTO {
id: OrderId
userId: UserId // 外部キー:型安全
productId: ProductId // 外部キー:型安全
amount: CNY // 通貨ブランド(パターン3参照)
createdAt: ISODateString // 時間ブランド
}
// コンパイル時にID混用を防止
function getUser(id: UserId): UserDTO { /* ... */ }
function getOrder(id: OrderId): OrderDTO { /* ... */ }
// getUser(newOrderId) // ❌ コンパイルエラー
// getOrder(newUserId) // ❌ コンパイルエラー
getUser(newUserId) // ✅ 正しい
getOrder(newOrderId) // ✅ 正しい
パターン3:通貨単位型
// branded-currency.ts - 型安全な通貨システム
/**
* 通貨ブランド型
* 解決:金額単位の混用(分/元)、通貨の混用、精度の消失
*/
// ===== 1. 通貨ブランド定義 =====
type CNY = Brand<number, 'CNY'> // 人民元(分)
type USD = Brand<number, 'USD'> // 米ドル(セント)
type JPY = Brand<number, 'JPY'> // 日本円(円、小数なし)
// 精度付き金額ブランド
type CNY_Yuan = Brand<number, 'CNY_Yuan'> // 人民元(元)
// ===== 2. 通貨コンストラクタ =====
function createCNY(fen: number): CNY {
if (!Number.isInteger(fen)) {
throw new TypeError('CNY amount must be in fen (integer)')
}
if (fen < 0) {
throw new TypeError('CNY amount must be non-negative')
}
return fen as CNY
}
function createUSD(cents: number): USD {
if (!Number.isInteger(cents)) {
throw new TypeError('USD amount must be in cents (integer)')
}
if (cents < 0) {
throw new TypeError('USD amount must be non-negative')
}
return cents as USD
}
function createJPY(yen: number): JPY {
if (!Number.isInteger(yen)) {
throw new TypeError('JPY amount must be in yen (integer)')
}
if (yen < 0) {
throw new TypeError('JPY amount must be non-negative')
}
return yen as JPY
}
// ===== 3. 型安全な通貨演算 =====
function addCNY(a: CNY, b: CNY): CNY {
return createCNY((a as number) + (b as number))
}
function subtractCNY(a: CNY, b: CNY): CNY {
const result = (a as number) - (b as number)
if (result < 0) {
throw new RangeError('CNY subtraction result cannot be negative')
}
return createCNY(result)
}
function multiplyCNY(amount: CNY, factor: number): CNY {
return createCNY(Math.round((amount as number) * factor))
}
// ❌ コンパイルエラー:異なる通貨は直接演算不可
// function addCNY_USD(a: CNY, b: USD): CNY { ... } // 型不一致
// ===== 4. 通貨変換(為替レートが必要) =====
interface ExchangeRate {
from: string
to: string
rate: number
updatedAt: Date
}
function convertCNYtoUSD(cny: CNY, rate: ExchangeRate): USD {
if (rate.from !== 'CNY' || rate.to !== 'USD') {
throw new TypeError('Invalid exchange rate direction')
}
const usdCents = Math.round((cny as number) * rate.rate / 100)
return createUSD(usdCents)
}
// ===== 5. フォーマット出力 =====
function formatCNY(amount: CNY): string {
const yuan = (amount as number) / 100
return `¥${yuan.toFixed(2)}`
}
function formatUSD(amount: USD): string {
const dollars = (amount as number) / 100
return `$${dollars.toFixed(2)}`
}
function formatJPY(amount: JPY): string {
return `¥${amount as number}`
}
// ===== 6. 使用例 =====
const price = createCNY(9900) // 99.00元
const discount = createCNY(1000) // 10.00元
const finalPrice = subtractCNY(price, discount) // 89.00元
console.log(formatCNY(finalPrice)) // ¥89.00
// const wrongPrice = createUSD(9900) // USD型
// addCNY(price, wrongPrice) // ❌ コンパイルエラー:USDはCNYパラメータに代入不可
パターン4:状態機械型
// branded-state-machine.ts - 型安全な状態機械
/**
* 状態機械ブランド型
* 解決:不正な状態遷移、状態の漏れ、遷移副作用の無制約
*/
// ===== 1. ブランド状態定義 =====
type PendingOrder = Brand<'pending', 'PendingOrder'>
type PaidOrder = Brand<'paid', 'PaidOrder'>
type ShippedOrder = Brand<'shipped', 'ShippedOrder'>
type CompletedOrder = Brand<'completed', 'CompletedOrder'>
type CancelledOrder = Brand<'cancelled', 'CancelledOrder'>
// ===== 2. 型安全な状態遷移 =====
// 合法な状態遷移のみ許可
function payOrder(_order: PendingOrder): PaidOrder {
return 'paid' as PaidOrder
}
function shipOrder(_order: PaidOrder): ShippedOrder {
return 'shipped' as ShippedOrder
}
function completeOrder(_order: ShippedOrder): CompletedOrder {
return 'completed' as CompletedOrder
}
function cancelPendingOrder(_order: PendingOrder): CancelledOrder {
return 'cancelled' as CancelledOrder
}
function cancelPaidOrder(_order: PaidOrder): CancelledOrder {
return 'cancelled' as CancelledOrder
}
// ❌ コンパイルエラー:不正な遷移
// function unshipOrder(_order: CompletedOrder): ShippedOrder { ... } // この関数は存在しない
// function repayOrder(_order: ShippedOrder): PaidOrder { ... } // この関数は存在しない
// ===== 3. データ付きブランド状態 =====
interface OrderStateData<S extends string> {
status: Brand<S, `Order_${S}`>
updatedAt: Date
updatedBy: UserId
}
type PendingOrderState = OrderStateData<'pending'> & {
items: Array<{ productId: ProductId; quantity: PositiveInt }>
}
type PaidOrderState = OrderStateData<'paid'> & {
items: Array<{ productId: ProductId; quantity: PositiveInt }>
paidAt: Date
paymentMethod: 'alipay' | 'wechat' | 'card'
transactionId: string
}
type ShippedOrderState = OrderStateData<'shipped'> & {
items: Array<{ productId: ProductId; quantity: PositiveInt }>
paidAt: Date
shippedAt: Date
trackingNumber: string
carrier: string
}
// ===== 4. 状態遷移関数(データ付き) =====
function payOrderWithData(
order: PendingOrderState,
paymentMethod: 'alipay' | 'wechat' | 'card',
transactionId: string
): PaidOrderState {
return {
status: 'paid' as Brand<'paid', 'Order_paid'>,
updatedAt: new Date(),
updatedBy: order.updatedBy,
items: order.items,
paidAt: new Date(),
paymentMethod,
transactionId,
}
}
function shipOrderWithData(
order: PaidOrderState,
trackingNumber: string,
carrier: string
): ShippedOrderState {
return {
status: 'shipped' as Brand<'shipped', 'Order_shipped'>,
updatedAt: new Date(),
updatedBy: order.updatedBy,
items: order.items,
paidAt: order.paidAt,
shippedAt: new Date(),
trackingNumber,
carrier,
}
}
// ===== 5. 網羅チェック =====
type AnyOrderState = PendingOrderState | PaidOrderState | ShippedOrderState
function handleOrderState(state: AnyOrderState): string {
switch (state.status as string) {
case 'pending':
return '支払待ち'
case 'paid':
return '支払済み、発送待ち'
case 'shipped':
return '発送済み、受取確認待ち'
// 状態が漏れている場合、TypeScriptはコンパイル時にエラーを出す
default:
const _exhaustive: never = state.status
throw new Error(`Unhandled state: ${_exhaustive}`)
}
}
パターン5:本番級Brand Typeツールライブラリ
// brand-toolkit.ts - 本番級Brand Typeツールライブラリ
/**
* 完全なBrand Typeツールキット
* カバー:作成、検証、シリアライズ、合成、マッピング
*/
// ===== 1. コアユーティリティ型 =====
/** 基底ブランド型 */
type Brand<T, B extends string> = T & { readonly __brand: B }
/** ブランドマーカーを抽出 */
type ExtractBrand<B> = B extends Brand<infer T, infer _>
? _ extends string ? _ : never
: never
/** 基底型を抽出 */
type Unbrand<B> = B extends Brand<infer T, infer _> ? T : B
/** ブランド型かどうかを判定 */
type IsBranded<T> = T extends Brand<any, any> ? true : false
// ===== 2. ブランド作成ツール =====
interface BrandCreatorConfig<T, B extends string> {
name: B
validate?: (value: T) => boolean
normalize?: (value: T) => T
errorMessage?: string
}
function createBrandCreator<T, B extends string>(
config: BrandCreatorConfig<T, B>
) {
const { name, validate, normalize, errorMessage } = config
function create(value: T): Brand<T, B> {
const normalized = normalize ? normalize(value) : value
if (validate && !validate(normalized)) {
throw new TypeError(
errorMessage ?? `Invalid value for brand "${name}": ${String(normalized)}`
)
}
return normalized as Brand<T, B>
}
function isType(value: T): value is Brand<T, B> {
return validate ? validate(value) : true
}
function unsafeCast(value: T): Brand<T, B> {
return value as Brand<T, B>
}
function unwrap(branded: Brand<T, B>): T {
return branded as T
}
return { create, isType, unsafeCast, unwrap, name }
}
// ===== 3. 一般的なブランドクリエイター =====
// 電話番号ブランド
const PhoneNumber = createBrandCreator<string, 'PhoneNumber'>({
name: 'PhoneNumber',
validate: (v) => /^1[3-9]\d{9}$/.test(v),
normalize: (v) => v.replace(/\s/g, ''),
errorMessage: 'Invalid Chinese phone number format',
})
// URLブランド
const SecureUrl = createBrandCreator<string, 'SecureUrl'>({
name: 'SecureUrl',
validate: (v) => /^https:\/\//.test(v),
errorMessage: 'URL must use HTTPS protocol',
})
// 非空文字列ブランド
const NonEmptyString = createBrandCreator<string, 'NonEmptyString'>({
name: 'NonEmptyString',
validate: (v) => v.trim().length > 0,
normalize: (v) => v.trim(),
errorMessage: 'String must not be empty',
})
// 正の整数ブランド
const PositiveInteger = createBrandCreator<number, 'PositiveInteger'>({
name: 'PositiveInteger',
validate: (v) => Number.isInteger(v) && v > 0,
errorMessage: 'Value must be a positive integer',
})
// ===== 4. ブランド合成 =====
type BrandedEmail = Brand<string, 'Email'> & Brand<string, 'NonEmpty'>
const BrandedEmailCreator = {
create(value: string): BrandedEmail {
const trimmed = value.trim()
if (trimmed.length === 0) {
throw new TypeError('Email must not be empty')
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
throw new TypeError('Invalid email format')
}
return trimmed as BrandedEmail
},
unwrap(value: BrandedEmail): string {
return value as string
},
}
// ===== 5. ブランドマッピングツール =====
function mapBranded<T, B extends string, R>(
branded: Brand<T, B>,
mapper: (value: T) => R
): R {
return mapper(branded as T)
}
function mapBrandedTo<T, B extends string, R, RB extends string>(
branded: Brand<T, B>,
mapper: (value: T) => R,
targetBrand: RB
): Brand<R, RB> {
return mapper(branded as T) as Brand<R, RB>
}
// 使用例
const userId = UserIdFactory.create('usr_abc12345')
const userIdLength = mapBranded(userId, (id) => id.length) // number
// ===== 6. ブランド型ガードコレクション =====
function createTypeGuard<T, B extends string>(
validate: (value: T) => boolean
): (value: T) => value is Brand<T, B> {
return (value: T): value is Brand<T, B> => validate(value)
}
const isUserId = createTypeGuard<string, 'UserId'>(
(v) => /^usr_[a-zA-Z0-9]{8,}$/.test(v)
)
const isOrderId = createTypeGuard<string, 'OrderId'>(
(v) => /^ord_[a-zA-Z0-9]{8,}$/.test(v)
)
// 使用
function processUnknownId(raw: string) {
if (isUserId(raw)) {
getUser(raw) // raw: UserId
} else if (isOrderId(raw)) {
getOrder(raw) // raw: OrderId
}
}
// ===== 7. ブランド型レジストリ =====
class BrandRegistry {
private creators = new Map<string, ReturnType<typeof createBrandCreator<any, any>>>()
register<T, B extends string>(name: B, creator: ReturnType<typeof createBrandCreator<T, B>>): void {
this.creators.set(name, creator)
}
get<B extends string>(name: B): ReturnType<typeof createBrandCreator<any, B>> | undefined {
return this.creators.get(name)
}
validate(name: string, value: unknown): boolean {
const creator = this.creators.get(name)
if (!creator) return false
try {
creator.create(value)
return true
} catch {
return false
}
}
}
// グローバル登録
const brandRegistry = new BrandRegistry()
brandRegistry.register('PhoneNumber', PhoneNumber)
brandRegistry.register('SecureUrl', SecureUrl)
brandRegistry.register('NonEmptyString', NonEmptyString)
brandRegistry.register('PositiveInteger', PositiveInteger)
5つの落とし穴回避ガイド
落とし穴1:ブランド型アサーションの濫用
// ❌ 誤り:あちこちでasアサーションを使用して型チェックを回避
const userId = 'raw_string' as UserId // 検証をバイパス
// ✅ 正しい:コンストラクタ経由でのみブランド値を作成
const userId = UserIdFactory.create('usr_abc12345')
落とし穴2:ブランド型とジェネリクスの衝突
// ❌ 誤り:ジェネリックパラメータでブランド情報が消失
function processValue<T extends string>(value: T): string { return value }
processValue(userId) // stringを返す、UserIdブランドが消失
// ✅ 正しい:ブランド型の伝達を維持
function processBranded<T extends Brand<string, any>>(value: T): Unbrand<T> {
return value as Unbrand<T>
}
落とし穴3:条件型におけるブランド型の動作
// ❌ 誤り:条件型でブランド情報が消失する可能性
type UnwrapIfBranded<T> = T extends Brand<infer U, any> ? U : T
type Result = UnwrapIfBranded<UserId> // string、ブランド情報が消失
// ✅ 正しい:ブランドを保持する必要がある場合はアンラップしない
type KeepBrand<T> = T // そのまま保持
落とし穴4:ブランド型とJSONシリアライズ
// ❌ 誤り:JSON.parse後にブランド情報が消失
const json = JSON.stringify(user)
const parsed = JSON.parse(json) as UserDTO // 安全でない!実行時検証なし
// ✅ 正しい:Zodまたはカスタムパーサーで検証
const parsed = UserDTOSchema.parse(JSON.parse(json)) // 実行時検証 + ブランド型
落とし穴5:ブランド型のas constとリテラルの衝突
// ❌ 誤り:as constがブランドマーカーをリテラル型にする
const status = 'pending' as const as PendingOrder
// ✅ 正しい:ブランドコンストラクタが内部でアサーションを処理
const status = createPendingOrder() // 関数の戻り型が明確
エラートラブルシューティング表
| エラーメッセージ | 原因 | 解決策 |
|---|---|---|
Type 'string' is not assignable to type 'UserId' |
生の文字列をブランド型に直接代入不可 | ブランドコンストラクタを使用して値を作成 |
Argument of type 'OrderId' is not assignable to parameter of type 'UserId' |
異なるブランド型は非互換 | 間違ったID型を渡していないか確認 |
Type 'Brand<string, "UserId">' is not assignable to type 'string' |
ブランド型をプリミティブに直接代入不可 | unwrapまたはas stringでアンボックス |
Conversion of type 'string' to type 'UserId' may be a mistake |
直接asアサーションは安全でない可能性 | 直接アサーションではなくブランドコンストラクタを使用 |
Property '__brand' does not exist on type 'string' |
実行時にブランドマーカーにアクセス | ブランドマーカーはコンパイル時のみ存在、実行時は消去済み |
Type instantiation is excessively deep |
ブランド型のネストが深すぎる | ブランド合成レベルを簡素化、独立したブランドに分割 |
Cannot find module 'zod' |
Zodが未インストール | npm install zod |
This expression is not callable |
ブランドコンストラクタが正しくエクスポートされていない | モジュールのエクスポート/インポートパスを確認 |
Type 'never' is not assignable |
網羅チェックで状態が漏れている | 漏れている状態分岐を追加 |
Generic type 'Brand' requires 2 type argument(s) |
Brandにブランドマーカーパラメータが不足 | ブランドマーカー文字列を追加、例:Brand<string, 'MyBrand'> |
5つの高度な最適化テクニック
テクニック1:テンプレートリテラルブランド
// テンプレートリテラル型 + Brand:エンティティIDブランドを自動生成
type EntityId<Entity extends string> = Brand<string, `${Entity}Id`>
type UserId = EntityId<'User'> // Brand<string, 'UserId'>
type OrderId = EntityId<'Order'> // Brand<string, 'OrderId'>
type ProductId = EntityId<'Product'> // Brand<string, 'ProductId'>
// 汎用IDファクトリ
function createEntityIdFactory<Entity extends string>(entity: Entity) {
const prefix = entity.toLowerCase().slice(0, 3)
return createIdFactory<`${Entity}Id`>({
prefix,
validate: (v) => new RegExp(`^${prefix}_[a-zA-Z0-9]{8,}$`).test(v),
})
}
テクニック2:ブランド型とEffect-TS統合
// EffectのBrandモジュールを使用(より安全)
import { Brand } from 'effect'
// EffectのBrandはより完全な実行時検証を提供
const UserId = Brand.struct({
__brand: Brand.literal('UserId'),
value: Brand.string.pipe(Brand.pattern(/^usr_[a-zA-Z0-9]{8,}$/)),
})
type UserId = Brand.Brand.Construct<typeof UserId>
テクニック3:ブランド型とPrisma統合
// Prismaモデルでブランド型を使用
import { Prisma } from '@prisma/client'
// Prismaが生成する型はプリミティブ型、ユーティリティ型でブランドを追加
type BrandedUser = {
id: UserId
name: string
email: Email
createdAt: ISODateString
}
// Prisma結果 → ブランド型
function toBrandedUser(prismaUser: Prisma.UserGetPayload<{}>): BrandedUser {
return {
id: UserIdFactory.create(prismaUser.id),
name: prismaUser.name,
email: createEmail(prismaUser.email),
createdAt: prismaUser.createdAt as ISODateString,
}
}
テクニック4:ブランド型とtRPC統合
// tRPCプロシージャでブランド型を使用
import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: UserIdSchema }))
.query(async ({ input }): Promise<UserDTO> => {
// input.idはすでにUserIdブランド型
return getUserById(input.id)
}),
createOrder: t.procedure
.input(z.object({
userId: UserIdSchema,
productId: ProductIdSchema,
amount: z.number().int().positive().transform((v) => createCNY(v)),
}))
.mutation(async ({ input }): Promise<OrderDTO> => {
// すべてのIDと金額がブランド型
return createOrder(input.userId, input.productId, input.amount)
}),
})
テクニック5:ブランド型パフォーマンスモニタリング
// brand-perf.ts - ブランド型実行時検証パフォーマンスモニタリング
interface BrandValidationMetrics {
brandName: string
totalValidations: number
failedValidations: number
averageValidationTime: number // μs
}
class BrandPerfMonitor {
private metrics = new Map<string, {
total: number
failed: number
times: number[]
}>()
recordValidation(brandName: string, duration: number, success: boolean): void {
const metric = this.metrics.get(brandName) ?? { total: 0, failed: 0, times: [] }
metric.total++
if (!success) metric.failed++
metric.times.push(duration)
this.metrics.set(brandName, metric)
}
getMetrics(): BrandValidationMetrics[] {
return Array.from(this.metrics.entries()).map(([name, metric]) => ({
brandName: name,
totalValidations: metric.total,
failedValidations: metric.failed,
averageValidationTime:
metric.times.reduce((a, b) => a + b, 0) / metric.times.length,
}))
}
}
// ブランドコンストラクタをラップしてモニタリングを追加
function withPerfMonitor<T, B extends string>(
creator: ReturnType<typeof createBrandCreator<T, B>>,
monitor: BrandPerfMonitor
): typeof creator {
const originalCreate = creator.create.bind(creator)
return {
...creator,
create(value: T): Brand<T, B> {
const start = performance.now()
try {
const result = originalCreate(value)
const duration = (performance.now() - start) * 1000 // μs
monitor.recordValidation(creator.name, duration, true)
return result
} catch (error) {
const duration = (performance.now() - start) * 1000
monitor.recordValidation(creator.name, duration, false)
throw error
}
},
}
}
比較分析表
| 次元 | プリミティブ型 | Brand Type | Zod Brand | io-ts | Effect Brand |
|---|---|---|---|---|---|
| コンパイル時安全性 | ❌ | ✅ | ✅ | ✅ | ✅ |
| 実行時検証 | ❌ | 手動必要 | ✅ 内蔵 | ✅ 内蔵 | ✅ 内蔵 |
| 実行時オーバーヘッド | なし | なし | あり | あり | あり |
| 学習コスト | 低い | 中程度 | 中程度 | 高い | 中程度 |
| エコシステム統合 | 完全 | 適応必要 | tRPCフレンドリー | FPエコシステム | Effectエコシステム |
| シリアライズ | 自動 | 処理必要 | 自動 | 自動 | 自動 |
| 型推論 | 自動 | 注釈必要 | 自動 | 複雑 | 自動 |
| バンドルサイズ | 0 | 0 | ~13KB | ~8KB | ~50KB |
まとめ
TypeScript Brand Typeは、2026年の型安全プログラミングに必須のパターンです。核心ポイント:
- 基礎定義:交差型
T & { __brand: B }で名義型を実現、実行時オーバーヘッドゼロ - 型安全ID:IDファクトリパターンで作成・検証・型ガードを統一、ID混用を排除
- 通貨単位:ブランド型で通貨と単位を制約、異なる通貨の直接演算をコンパイル時に防止
- 状態機械:ブランド状態で合法な遷移パスを限定、網羅チェックで漏れを防止
- 本番級ツールライブラリ:ブランドクリエイター、レジストリ、Zod統合、パフォーマンスモニタリング、完全なライフサイクルをカバー
Brand Typeは、TypeScriptを「構造的互換性で十分」から「セマンティクスが違えば不可」へと進化させます。ID、金額、状態といった重要領域において、ブランド型は実行時バグに対する最初の防衛線です。
オンラインツールおすすめ
- /ja/json/format - JSONフォーマッター、ブランド型シリアライズ出力のデバッグ
- /ja/dev/curl-to-code - APIリクエストからコード変換、ブランド型インターフェースデバッグ
- /ja/encode/hash - ハッシュ計算、ブランドID一意性検証
- /ja/text/diff - テキスト差分、ブランド型定義変更の比較
ブラウザローカルツールを無料で試す →
#TypeScript Brand Type#类型安全#nominal typing#类型品牌#TypeScript 5.5+#2026#前端开发