TypeScript品牌类型实战:构建类型安全领域模型的6个核心模式
前端工程
类型安全的四大痛点
你定义了UserId和OrderId都是string,结果函数参数传反了编译器也不报错;邮箱地址和普通字符串可以互相赋值,运行时才发现格式不对;API返回的JSON用as User一断言就完事,类型安全形同虚设;领域模型中DraftOrder和ApprovedOrder共享同一个接口,状态转换全靠自觉。TypeScript的结构化类型系统让所有string都长得一样,领域概念在类型层面完全丢失。
本文将从核心概念出发,带你完成基础Branded Type→Zod验证集成→智能构造器→领域模型组合→序列化反序列化→不可变状态机的6个核心模式,构建编译时和运行时双重安全的领域模型。
核心概念
| 概念 | 说明 |
|---|---|
| Branded Type | 通过交叉类型为原始类型附加品牌标记,实现名义类型效果,如string & { __brand: 'UserId' } |
| Nominal Type | 名义类型系统,类型兼容性基于声明名称而非结构,TypeScript原生不支持,需通过Branded Type模拟 |
| Structural Type | 结构化类型系统,TypeScript默认采用,只要结构兼容即可赋值,UserId和OrderId无法区分 |
| 品牌符号 | 品牌类型的唯一标识,通常用__brand或_brand等不可达属性名,运行时不存在 |
| 类型收窄 | 通过类型守卫将宽泛类型缩小为品牌类型,在控制流中精确区分不同品牌 |
| Zod验证 | 运行时类型校验库,与品牌类型结合实现编译时+运行时双重安全 |
| 类型守卫 | 返回类型谓词的函数val is BrandType,在运行时检查的同时收窄编译时类型 |
| 不可变类型 | 通过readonly和Readonly确保品牌类型实例不可变,防止领域对象被意外修改 |
品牌类型工作流
编译时安全:
Branded Type定义 → 智能构造器(验证) → 类型守卫(收窄) → 领域模型组合
运行时安全:
Zod Schema验证 → 构造器校验 → 序列化/反序列化 → 状态机约束
端到端安全:
API边界Zod校验 → 品牌类型构造 → 业务逻辑类型约束 → 序列化输出验证
问题分析:品牌类型的5大挑战
- 结构化类型系统限制:TypeScript采用结构化类型,
type UserId = string和type OrderId = string完全等价,无法在编译时区分不同领域的同构类型,需要Branded Type模拟名义类型 - 运行时与编译时类型不一致:品牌标记
__brand在运行时被擦除,编译时精确的品牌类型无法保证运行时数据的合法性,API边界必须配合运行时校验 - 品牌类型序列化困难:JSON序列化时品牌标记丢失,反序列化后需要重新构造品牌类型,网络传输和持久化场景需要特殊处理
- 泛型约束复杂:品牌类型与泛型结合时约束表达困难,
<T extends Brand<string>>的推导和收窄需要精心设计类型关系 - 库类型扩展受限:第三方库的类型定义无法直接添加品牌标记,需要通过模块增强和类型映射间接扩展
分步实操:6个核心模式
模式1:基础Branded Type定义与使用
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type Email = Brand<string, 'Email'>;
type PositiveInt = Brand<number, 'PositiveInt'>;
function createUserId(value: string): UserId {
return value as UserId;
}
function createOrderId(value: string): OrderId {
return value as OrderId;
}
const userId: UserId = createUserId('user-001');
const orderId: OrderId = createOrderId('order-001');
// ❌ 编译错误:不能将OrderId赋值给UserId
// const wrong: UserId = orderId;
function getUser(id: UserId): string {
return `User: ${id}`;
}
getUser(userId);
// ❌ getUser(orderId); // 类型错误,OrderId不能赋值给UserId
模式2:品牌类型与Zod验证集成
import { z } from 'zod';
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
type Age = Brand<number, 'Age'>;
const UserIdSchema = z.string().uuid().transform((v) => v as UserId);
const EmailSchema = z.string().email().transform((v) => v as Email);
const AgeSchema = z.number().int().min(0).max(150).transform((v) => v as Age);
function parseUserId(value: unknown): UserId {
return UserIdSchema.parse(value);
}
function parseEmail(value: unknown): Email {
return EmailSchema.parse(value);
}
function parseAge(value: unknown): Age {
return AgeSchema.parse(value);
}
const userId = parseUserId('550e8400-e29b-41d4-a716-446655440000');
const email = parseEmail('user@example.com');
const age = parseAge(25);
// ❌ parseUserId('not-a-uuid'); // ZodError
// ❌ parseEmail('invalid-email'); // ZodError
// ❌ parseAge(-1); // ZodError
const UserSchema = z.object({
id: UserIdSchema,
email: EmailSchema,
age: AgeSchema,
name: z.string().min(1),
});
type User = z.infer<typeof UserSchema>;
function safeParseUser(data: unknown) {
return UserSchema.safeParse(data);
}
模式3:品牌类型智能构造器
type Brand<T, B extends string> = T & { readonly __brand: B };
type Result<T, E = string> =
| { success: true; value: T }
| { success: false; error: E };
class BrandConstructor<T, B extends string> {
constructor(
private readonly brand: B,
private readonly validate: (value: T) => boolean,
private readonly errorMessage: string,
) {}
create(value: T): Brand<T, B> {
if (!this.validate(value)) {
throw new Error(`${this.brand}: ${this.errorMessage}`);
}
return value as Brand<T, B>;
}
safeCreate(value: T): Result<Brand<T, B>> {
if (!this.validate(value)) {
return { success: false, error: `${this.brand}: ${this.errorMessage}` };
}
return { success: true, value: value as Brand<T, B> };
}
isBrand(value: T): value is Brand<T, B> {
return this.validate(value);
}
unwrap(value: Brand<T, B>): T {
return value as T;
}
}
type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
type PositiveInt = Brand<number, 'PositiveInt'>;
const UserIdBrand = new BrandConstructor<string, 'UserId'>(
'UserId',
(v) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v),
'Invalid UUID format',
);
const EmailBrand = new BrandConstructor<string, 'Email'>(
'Email',
(v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
'Invalid email format',
);
const PositiveIntBrand = new BrandConstructor<number, 'PositiveInt'>(
'PositiveInt',
(v) => Number.isInteger(v) && v > 0,
'Must be a positive integer',
);
const userId = UserIdBrand.create('550e8400-e29b-41d4-a716-446655440000');
const emailResult = EmailBrand.safeCreate('user@example.com');
const ageResult = PositiveIntBrand.safeCreate(-5);
if (emailResult.success) {
console.log(emailResult.value);
}
模式4:领域模型类型安全组合
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
type Money = Brand<number, 'Money'>;
type SKU = Brand<string, 'SKU'>;
interface OrderLine {
sku: SKU;
quantity: PositiveInt;
unitPrice: Money;
}
interface DraftOrder {
readonly status: 'draft';
readonly id: OrderId;
readonly customerId: UserId;
readonly lines: readonly OrderLine[];
readonly createdAt: Date;
}
interface ApprovedOrder {
readonly status: 'approved';
readonly id: OrderId;
readonly customerId: UserId;
readonly lines: readonly OrderLine[];
readonly approvedBy: UserId;
readonly approvedAt: Date;
}
interface ShippedOrder {
readonly status: 'shipped';
readonly id: OrderId;
readonly customerId: UserId;
readonly lines: readonly OrderLine[];
readonly approvedBy: UserId;
readonly trackingNumber: string;
readonly shippedAt: Date;
}
type Order = DraftOrder | ApprovedOrder | ShippedOrder;
function approveOrder(order: DraftOrder, approvedBy: UserId): ApprovedOrder {
return {
...order,
status: 'approved',
approvedBy,
approvedAt: new Date(),
};
}
function shipOrder(order: ApprovedOrder, trackingNumber: string): ShippedOrder {
return {
...order,
status: 'shipped',
trackingNumber,
shippedAt: new Date(),
};
}
// ❌ approveOrder(approvedOrder, userId); // 类型错误,ApprovedOrder不能赋值给DraftOrder
// ❌ shipOrder(draftOrder, 'tracking'); // 类型错误,DraftOrder不能赋值给ApprovedOrder
function getOrderTotal(order: Order): Money {
const total = order.lines.reduce(
(sum, line) => sum + (line.unitPrice as number) * (line.quantity as number),
0,
);
return total as Money;
}
模式5:品牌类型序列化与反序列化
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
interface SerializableBrand<T, B extends string> {
serialize(value: Brand<T, B>): T;
deserialize(raw: unknown): Brand<T, B>;
}
function createSerializableBrand<T, B extends string>(
brand: B,
validate: (value: T) => boolean,
): SerializableBrand<T, B> {
return {
serialize(value: Brand<T, B>): T {
return value as T;
},
deserialize(raw: unknown): Brand<T, B> {
if (typeof raw !== 'string' || !validate(raw as T)) {
throw new Error(`Invalid ${brand} value: ${String(raw)}`);
}
return raw as Brand<T, B>;
},
};
}
const UserIdSerDe = createSerializableBrand<string, 'UserId'>(
'UserId',
(v) => /^[0-9a-f]{8}-[0-9a-f]{4}/i.test(v),
);
const EmailSerDe = createSerializableBrand<string, 'Email'>(
'Email',
(v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
);
interface UserDTO {
id: string;
email: string;
name: string;
}
interface User {
readonly id: UserId;
readonly email: Email;
readonly name: string;
}
function toDTO(user: User): UserDTO {
return {
id: UserIdSerDe.serialize(user.id),
email: EmailSerDe.serialize(user.email),
name: user.name,
};
}
function fromDTO(dto: unknown): User {
if (typeof dto !== 'object' || dto === null) {
throw new Error('Invalid user DTO');
}
const raw = dto as Record<string, unknown>;
return {
id: UserIdSerDe.deserialize(raw.id),
email: EmailSerDe.deserialize(raw.email),
name: String(raw.name),
};
}
const user: User = {
id: '550e8400-e29b-41d4-a716-446655440000' as UserId,
email: 'user@example.com' as Email,
name: 'Alice',
};
const dto = toDTO(user);
const restored = fromDTO(dto);
模式6:不可变品牌类型与状态机
type Brand<T, B extends string> = T & { readonly __brand: B };
type OrderId = Brand<string, 'OrderId'>;
type UserId = Brand<string, 'UserId'>;
type Draft = 'draft';
type Approved = 'approved';
type Shipped = 'shipped';
type Cancelled = 'cancelled';
interface OrderState<S extends string> {
readonly _tag: S;
}
interface DraftOrder extends OrderState<Draft> {
readonly id: OrderId;
readonly customerId: UserId;
readonly items: readonly string[];
}
interface ApprovedOrder extends OrderState<Approved> {
readonly id: OrderId;
readonly customerId: UserId;
readonly items: readonly string[];
readonly approvedBy: UserId;
readonly approvedAt: Date;
}
interface ShippedOrder extends OrderState<Shipped> {
readonly id: OrderId;
readonly customerId: UserId;
readonly items: readonly string[];
readonly approvedBy: UserId;
readonly trackingNumber: string;
readonly shippedAt: Date;
}
interface CancelledOrder extends OrderState<Cancelled> {
readonly id: OrderId;
readonly customerId: UserId;
readonly reason: string;
readonly cancelledAt: Date;
}
type AnyOrder = DraftOrder | ApprovedOrder | ShippedOrder | CancelledOrder;
type TransitionMap = {
draft: { approve: Approved; cancel: Cancelled };
approved: { ship: Shipped; cancel: Cancelled };
shipped: {};
cancelled: {};
};
type NextState<S extends string, E extends string> =
S extends keyof TransitionMap
? E extends keyof TransitionMap[S]
? TransitionMap[S][E]
: never
: never;
function transition<S extends keyof TransitionMap, E extends keyof TransitionMap[S]>(
order: AnyOrder & OrderState<S>,
event: E,
data: TransitionPayload<S, E>,
): AnyOrder & OrderState<NextState<S, E>> {
switch (event) {
case 'approve':
return {
...order,
_tag: 'approved',
approvedBy: (data as { approvedBy: UserId }).approvedBy,
approvedAt: new Date(),
} as AnyOrder & OrderState<NextState<S, E>>;
case 'ship':
return {
...order,
_tag: 'shipped',
trackingNumber: (data as { trackingNumber: string }).trackingNumber,
shippedAt: new Date(),
} as AnyOrder & OrderState<NextState<S, E>>;
case 'cancel':
return {
id: order.id,
customerId: order.customerId,
_tag: 'cancelled',
reason: (data as { reason: string }).reason,
cancelledAt: new Date(),
} as AnyOrder & OrderState<NextState<S, E>>;
default:
throw new Error(`Invalid event: ${String(event)}`);
}
}
type TransitionPayload<S extends string, E extends string> =
E extends 'approve' ? { approvedBy: UserId } :
E extends 'ship' ? { trackingNumber: string } :
E extends 'cancel' ? { reason: string } :
never;
避坑指南
陷阱1:直接用type别名代替品牌类型
// ❌ type别名不创建新类型,UserId和string完全等价
type UserId = string;
type OrderId = string;
const userId: UserId = '123';
const orderId: OrderId = userId; // 编译通过!类型安全丢失
// ✅ 使用Brand交叉类型创建名义类型
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
const orderId: OrderId = userId as UserId; // ❌ 编译错误
陷阱2:品牌标记使用可达属性
// ❌ 可达属性会在运行时产生副作用,影响序列化和比较
type BadBrand<T, B extends string> = T & { brand: B };
const id: BadBrand<string, 'UserId'> = Object.assign('123', { brand: 'UserId' });
JSON.stringify(id); // "123" — 品牌标记丢失
// ✅ 使用不可达的品牌符号属性
type Brand<T, B extends string> = T & { readonly __brand: B };
陷阱3:跳过运行时验证直接断言
// ❌ 直接as断言绕过验证,运行时数据可能不合法
const email = 'not-an-email' as Email;
// ✅ 通过构造器或Zod验证确保运行时合法性
const email = EmailBrand.create('user@example.com');
陷阱4:品牌类型在泛型中丢失品牌信息
// ❌ 泛型推导时品牌信息可能被擦除
function first<T>(arr: T[]): T { return arr[0]; }
const ids: UserId[] = [createUserId('1')];
const id = first(ids); // UserId ✓ 但如果泛型约束不当可能退化为string
// ✅ 使用品牌类型作为泛型约束
function firstBrand<T extends Brand<string, string>>(arr: T[]): T { return arr[0]; }
陷阱5:序列化时忘记反序列化验证
// ❌ 从JSON.parse直接断言品牌类型,跳过验证
const user = JSON.parse(json) as User;
// ✅ 通过SerDe反序列化,验证数据合法性
const user = fromDTO(JSON.parse(json));
报错排查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
Type 'string' is not assignable to type 'Brand<string, "X">' |
直接赋值原始值给品牌类型 | 使用构造器或类型断言创建品牌类型 |
Type 'Brand<string, "A">' is not assignable to type 'Brand<string, "B">' |
不同品牌类型互相赋值 | 检查品牌标记是否一致,确认类型对应关系 |
Property '__brand' does not exist on type 'string' |
对原始类型访问品牌属性 | 品牌属性仅用于编译时区分,运行时不存在 |
| Zod验证通过但类型不匹配 | z.transform返回类型与品牌类型不一致 |
确保transform回调返回value as BrandType |
| 序列化后品牌信息丢失 | JSON.stringify忽略不可枚举属性 | 品牌标记设计为不可达属性,序列化只需原始值 |
泛型中品牌类型推导为unknown |
泛型约束缺少品牌类型边界 | 使用T extends Brand<string, string>约束 |
| 类型守卫不收窄 | is谓词类型与实际品牌类型不一致 |
确保类型守卫返回value is BrandType精确匹配 |
模块增强报__brand不存在 |
第三方类型定义未声明品牌属性 | 通过declare module扩展类型定义 |
交叉类型产生never |
品牌标记与已有属性冲突 | 使用独特的品牌属性名如__brand_X避免冲突 |
| 运行时类型检查缺失 | 仅依赖编译时品牌类型 | 在API边界使用Zod或构造器进行运行时验证 |
进阶优化
品牌类型工具库
type Brand<T, B extends string> = T & { readonly __brand: B };
type Unbrand<T> = T extends Brand<infer U, string> ? U : T;
type BrandOf<T> = T extends Brand<any, infer B> ? B : never;
type IsBranded<T> = T extends Brand<any, string> ? true : false;
type ReplaceBrand<T, B extends string> = T extends Brand<infer U, string> ? Brand<U, B> : never;
type BrandedKeys<T> = { [K in keyof T]: IsBranded<T[K]> extends true ? K : never }[keyof T];
条件品牌类型
type MaybeBrand<T, B extends string, ShouldBrand extends boolean> =
ShouldBrand extends true ? Brand<T, B> : T;
type ConfigValue<T, Branded extends boolean = true> = {
id: MaybeBrand<string, 'ConfigId', Branded>;
value: T;
updatedAt: Date;
};
品牌类型与Effect系统集成
type Brand<T, B extends string> = T & { readonly __brand: B };
type Validated<T> = Brand<T, 'Validated'>;
type Sanitized<T> = Brand<T, 'Sanitized'>;
function validate<T>(value: T, predicate: (v: T) => boolean): Validated<T> | Error {
return predicate(value) ? value as Validated<T> : new Error('Validation failed');
}
function sanitize(input: string): Sanitized<string> {
return input.replace(/[<>"'&]/g, '') as Sanitized<string>;
}
对比分析
| 特性 | Branded Type | Opaque Type | io-ts | runtypes |
|---|---|---|---|---|
| 实现方式 | 交叉类型+品牌属性 | 条件类型+唯一符号 | 运行时编解码器 | 运行时类型检查 |
| 编译时安全 | ★★★★★ | ★★★★★ | ★★★★ | ★★★★ |
| 运行时安全 | 需配合Zod | 需配合Zod | ★★★★★ | ★★★★★ |
| 学习成本 | 低 | 中 | 高 | 中 |
| 包体积 | 0 | 0 | ~15KB | ~8KB |
| 序列化支持 | 需手动实现 | 需手动实现 | ★★★★★ | ★★★★ |
| 泛型支持 | ★★★★ | ★★★ | ★★★★ | ★★★ |
| 生态集成 | Zod/Effect | Zod | fp-ts | — |
| TypeScript原生 | 是 | 是 | 否 | 否 |
| 生产推荐 | ★★★★★ | ★★★★ | ★★★ | ★★★ |
品牌类型不是"类型体操"的花架子,而是领域驱动设计的类型安全基石。 它让
UserId和OrderId在编译时泾渭分明,让Zod验证在运行时守卫数据边界,让领域模型的状态转换在类型层面可验证。类型安全的最高境界不是写出最复杂的类型,而是让非法状态不可表达。
推荐工具
本站提供浏览器本地工具,免注册即可试用 →
#TypeScript品牌类型#Branded Type#类型安全#Nominal Type#类型编程#2026#前端工程