TypeScript品牌类型实战:构建类型安全领域模型的6个核心模式

前端工程

类型安全的四大痛点

你定义了UserIdOrderId都是string,结果函数参数传反了编译器也不报错;邮箱地址和普通字符串可以互相赋值,运行时才发现格式不对;API返回的JSON用as User一断言就完事,类型安全形同虚设;领域模型中DraftOrderApprovedOrder共享同一个接口,状态转换全靠自觉。TypeScript的结构化类型系统让所有string都长得一样,领域概念在类型层面完全丢失

本文将从核心概念出发,带你完成基础Branded Type→Zod验证集成→智能构造器→领域模型组合→序列化反序列化→不可变状态机的6个核心模式,构建编译时和运行时双重安全的领域模型。


核心概念

概念 说明
Branded Type 通过交叉类型为原始类型附加品牌标记,实现名义类型效果,如string & { __brand: 'UserId' }
Nominal Type 名义类型系统,类型兼容性基于声明名称而非结构,TypeScript原生不支持,需通过Branded Type模拟
Structural Type 结构化类型系统,TypeScript默认采用,只要结构兼容即可赋值,UserIdOrderId无法区分
品牌符号 品牌类型的唯一标识,通常用__brand_brand等不可达属性名,运行时不存在
类型收窄 通过类型守卫将宽泛类型缩小为品牌类型,在控制流中精确区分不同品牌
Zod验证 运行时类型校验库,与品牌类型结合实现编译时+运行时双重安全
类型守卫 返回类型谓词的函数val is BrandType,在运行时检查的同时收窄编译时类型
不可变类型 通过readonlyReadonly确保品牌类型实例不可变,防止领域对象被意外修改

品牌类型工作流

编译时安全:
Branded Type定义 → 智能构造器(验证) → 类型守卫(收窄) → 领域模型组合

运行时安全:
Zod Schema验证 → 构造器校验 → 序列化/反序列化 → 状态机约束

端到端安全:
API边界Zod校验 → 品牌类型构造 → 业务逻辑类型约束 → 序列化输出验证

问题分析:品牌类型的5大挑战

  1. 结构化类型系统限制:TypeScript采用结构化类型,type UserId = stringtype OrderId = string完全等价,无法在编译时区分不同领域的同构类型,需要Branded Type模拟名义类型
  2. 运行时与编译时类型不一致:品牌标记__brand在运行时被擦除,编译时精确的品牌类型无法保证运行时数据的合法性,API边界必须配合运行时校验
  3. 品牌类型序列化困难:JSON序列化时品牌标记丢失,反序列化后需要重新构造品牌类型,网络传输和持久化场景需要特殊处理
  4. 泛型约束复杂:品牌类型与泛型结合时约束表达困难,<T extends Brand<string>>的推导和收窄需要精心设计类型关系
  5. 库类型扩展受限:第三方库的类型定义无法直接添加品牌标记,需要通过模块增强和类型映射间接扩展

分步实操: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原生
生产推荐 ★★★★★ ★★★★ ★★★ ★★★

品牌类型不是"类型体操"的花架子,而是领域驱动设计的类型安全基石。 它让UserIdOrderId在编译时泾渭分明,让Zod验证在运行时守卫数据边界,让领域模型的状态转换在类型层面可验证。类型安全的最高境界不是写出最复杂的类型,而是让非法状态不可表达


推荐工具

  • JSON格式化 — 格式化API响应JSON,快速排查品牌类型序列化问题
  • 代码格式化 — 格式化TypeScript代码,统一品牌类型定义风格
  • cURL转代码 — 将API请求转为类型安全的TypeScript品牌类型fetch代码

本站提供浏览器本地工具,免注册即可试用 →

#TypeScript品牌类型#Branded Type#类型安全#Nominal Type#类型编程#2026#前端工程