TypeScript Brand Type:型安全IDと型ブランド実践

前端开发

TypeScript Brand Type:型安全IDと型ブランド実践

TypeScriptは構造的型システムです——形が同じなら型は互換します。つまり、UserIdOrderIdがどちらも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年の型安全プログラミングに必須のパターンです。核心ポイント:

  1. 基礎定義:交差型T & { __brand: B }で名義型を実現、実行時オーバーヘッドゼロ
  2. 型安全ID:IDファクトリパターンで作成・検証・型ガードを統一、ID混用を排除
  3. 通貨単位:ブランド型で通貨と単位を制約、異なる通貨の直接演算をコンパイル時に防止
  4. 状態機械:ブランド状態で合法な遷移パスを限定、網羅チェックで漏れを防止
  5. 本番級ツールライブラリ:ブランドクリエイター、レジストリ、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#前端开发