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#前端开发