TypeScript Brand Type:型別安全ID與型別品牌實戰

前端开发

TypeScript Brand Type:型別安全ID與型別品牌實戰

TypeScript是結構化型別系統——只要形狀一樣,型別就相容。這意味著UserIdOrderId都是string時可以互相賦值,編譯器不會報錯。這在業務程式碼中是災難:把使用者ID傳給訂單查詢介面,執行時才發現錯誤。

Brand Type(型別品牌)是TypeScript中實現名義型別(Nominal Typing)的經典模式。它透過在型別上「烙印」一個唯一標記,讓結構相同的型別在編譯期就不相容。本文從基礎到生產級,給你一套完整的Brand Type實戰方案。

核心概念速覽

概念 結構化型別 Brand Type(名義型別) 差異
型別相容 結構相同即相容 即使結構相同也不相容 ⭐⭐⭐⭐⭐
編譯期檢查 弱(ID混用不報錯) 強(ID混用立即報錯) ⭐⭐⭐⭐⭐
執行時開銷 無(型別擦除)
程式碼可讀性 弱(都是string) 強(語義明確) ⭐⭐⭐⭐
重構安全性 高(型別變更自動傳播) ⭐⭐⭐⭐⭐
學習成本 ⭐⭐⭐

五大痛點分析

痛點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) // 編譯通過!但語義完全錯誤

五大核心模式實操

模式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('zhang@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. ID在API型別中的使用 =====

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)      // ✅ 正確
// branded-id-serialization.ts - ID序列化與反序列化

/**
 * 品牌ID的序列化/反序列化
 * 處理API邊界:JSON → 品牌型別 → JSON
 */

// 序列化:品牌型別 → 原始型別(自動,型別擦除)
function serializeUser(user: UserDTO): object {
  return {
    id: user.id as string,          // Brand → string
    name: user.name,
    email: user.email as string,    // Brand → string
    tenantId: user.tenantId as string,
  }
}

// 反序列化:原始型別 → 品牌型別(需要驗證)
function deserializeUser(raw: unknown): UserDTO {
  if (typeof raw !== 'object' || raw === null) {
    throw new TypeError('Invalid user data')
  }
  const data = raw as Record<string, unknown>

  return {
    id: UserIdFactory.create(data.id as string),
    name: data.name as string,
    email: createEmail(data.email as string),
    tenantId: data.tenantId as TenantId,
  }
}

// Zod整合:執行時驗證 + 編譯期型別安全
import { z } from 'zod'

const UserIdSchema = z.string().regex(/^usr_[a-zA-Z0-9]{8,}$/).transform((v) => UserIdFactory.create(v))
const OrderIdSchema = z.string().regex(/^ord_[a-zA-Z0-9]{8,}$/).transform((v) => OrderIdFactory.create(v))
const EmailSchema = z.string().email().transform((v) => createEmail(v))

const UserDTOSchema = z.object({
  id: UserIdSchema,
  name: z.string().min(1),
  email: EmailSchema,
  tenantId: z.string().transform((v) => v as TenantId),
})

// API回應解析
function parseUserResponse(data: unknown): UserDTO {
  return UserDTOSchema.parse(data) // 執行時驗證 + 品牌型別
}

模式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)

五大避坑指南

坑1:品牌型別斷言濫用

// ❌ 錯誤:到處使用as斷言繞過型別檢查
const userId = 'raw_string' as UserId // 繞過了驗證

// ✅ 正確:只透過建構器建立品牌值
const userId = UserIdFactory.create('usr_abc12345')

坑2:品牌型別與泛型衝突

// ❌ 錯誤:泛型參數遺失品牌資訊
function firstElement<T>(arr: T[]): T { return arr[0] }
const ids: UserId[] = [UserIdFactory.create('usr_1')]
const result = firstElement(ids) // T = UserId,OK

// 但如果泛型推斷為string,品牌資訊遺失
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 user: UserDTO = { id: userId, name: '老張', email: createEmail('z@t.com'), tenantId: 't1' as TenantId }
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 // 型別變成 'pending' & { __brand: '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'>

五大進階優化技巧

技巧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、金額、狀態這些關鍵領域,品牌型別是防止執行時bug的第一道防線。

線上工具推薦

本站提供瀏覽器本地工具,免註冊即可試用 →

#TypeScript Brand Type#类型安全#nominal typing#类型品牌#TypeScript 5.5+#2026#前端开发