TypeScript 类型编程:Brand Types 实战

通过 Brand Types 为原始类型添加语义标记,防止 UserId 和 PostId 混用,让编译器在构建期捕获此类错误。

问题:原始类型太宽松

// 危险:两个 string 参数顺序容易搞反
function assignPost(userId: string, postId: string) { ... }

assignPost(post.id, user.id); // 编译器不报错,运行时出 bug

解决:Brand Types

Brand Types 通过 TypeScript 的结构化类型系统,在运行时相同的类型上添加编译期标记:

type Brand<T, B> = T & { readonly __brand: B };

type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;

function assignPost(userId: UserId, postId: PostId) { ... }

// ✅ 正确用法
const uid = 'u_123' as UserId;
const pid = 'p_456' as PostId;
assignPost(uid, pid);

// ❌ 编译期报错:不能将 PostId 赋给 UserId
assignPost(pid, uid);

实际使用场景

场景类型
用户 IDBrand<string, 'UserId'>
文章 slugBrand<string, 'PostSlug'>
金额(分)Brand<number, 'Cents'>
邮箱Brand<string, 'Email'>

工厂函数

生产代码中通过工厂函数创建 Brand 值,避免手动 as 断言:

function createUserId(raw: string): UserId {
  // 这里可以加验证逻辑
  return raw as UserId;
}

小结

Brand Types 是零运行时开销的类型安全增强——编译期捕获语义错误,生产 JS 输出与原始代码无异。