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) | 强(语义明确) | ⭐⭐⭐⭐ |
| 重构安全性 | 低 | 高(类型变更自动传播) | ⭐⭐⭐⭐⭐ |
| 学习成本 | 低 | 中 | ⭐⭐⭐ |
五大痛点分析
痛点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年类型安全编程的必备模式。核心要点:
- 基础定义:通过交叉类型
T & { __brand: B }实现名义类型,零运行时开销 - 类型安全ID:ID工厂模式统一创建、验证、类型守卫,杜绝ID混用
- 货币单位:品牌类型约束币种和单位,编译期防止不同货币直接运算
- 状态机:品牌状态限定合法转换路径,穷举检查确保无遗漏
- 生产级工具库:品牌创建器、注册表、Zod集成、性能监控,覆盖完整生命周期
Brand Type让TypeScript从"结构兼容就行"进化到"语义不对就不行"。在ID、金额、状态这些关键领域,品牌类型是防止运行时bug的第一道防线。
在线工具推荐
- /zh-CN/json/format - JSON格式化,调试品牌类型序列化输出
- /zh-CN/dev/curl-to-code - API请求转代码,品牌类型接口调试
- /zh-CN/encode/hash - 哈希计算,品牌ID唯一性校验
- /zh-CN/text/diff - 文本对比,对比品牌类型定义变更
本站提供浏览器本地工具,免注册即可试用 →
#TypeScript Brand Type#类型安全#nominal typing#类型品牌#TypeScript 5.5+#2026#前端开发