拒绝“都是 string”:品牌类型与领域驱动设计 (DDD)

📝 引言

📌 场景/痛点

在业务代码中,我们经常看到这样的函数签名:

typescript 复制代码
function transferMoney(fromAccountId: string, toAccountId: string, amount: number) { ... }

这极其危险!

因为 fromAccountIdtoAccountId 都是 string,TS 编译器完全认为它们是同一种东西。一旦你在调用时传反了顺序,或者把 orderId 传给了 accountId,编译器屁都不会放一个,只有等到程序在运行时报错(甚至更糟,默默转错了账)。

问题根源 :TypeScript 是结构化类型 (鸭子类型),它只看"形状",不看"名字"。type A = stringtype B = string 是等价的。
我们需要的是"名义化类型",让业务上的不同概念在类型层面也严格区分。

✨ 最终效果

引入"品牌类型"后,你将获得业务级的类型安全。把 UserId 传给需要 OrderId 的参数,IDE 会直接亮红灯。

📖 内容概览

本文将带你掌握 TS 在业务建模中的高级用法:

  1. 核心痛点:为什么普通类型无法区分业务概念?
  2. 品牌类型:利用交叉类型实现"名义化"。
  3. 实战模式 :创建安全的 ID 工厂函数。
  4. DDD 思维:用类型系统映射业务领域。

🛠️ 正文

1. 核心概念:品牌类型

TS 本身不支持"名义类型",但我们可以通过"交叉类型"模拟它。

原理是:给一个基础类型(如 string)打上一个不可见的"标签"。

typescript 复制代码
// ✨ 品牌类型的通用定义
// T 是基础类型 (如 string, number)
// B 是品牌名 (如 'UserId')
export type Brand<T, B> = T & { readonly __brand: B };

2. 实战演练:构建安全的 ID 系统

假设我们有一个电商系统,我们需要严格区分 UserIdProductId

2.1 定义类型
typescript 复制代码
// src/types/brands.ts

// 基础定义:本质上还是 string,但带上了 'User' 的标签
export type UserId = Brand<string, 'User'>;
export type ProductId = Brand<string, 'Product'>;
2.2 定义工厂函数

为了防止有人直接写 const id: UserId = 'abc' (这实际上是被允许的,因为 'abc'string 的子集),我们应该使用工厂函数来强制创建。

typescript 复制代码
export const UserId = {
    create: (value: string): UserId => {
        // 在这里可以加入校验逻辑,比如格式检查
        if (!value.startsWith('user_')) {
            throw new Error('Invalid UserId format');
        }
        return value as UserId; // 类型断言
    }
};

export const ProductId = {
    create: (value: string): ProductId => {
        if (!value.startsWith('prod_')) {
            throw new Error('Invalid ProductId format');
        }
        return value as ProductId;
    }
};
2.3 业务应用

现在,看看类型系统如何保护我们。

typescript 复制代码
// 🚨 危险的旧代码
function deleteUser(id: string) { ... }
// deleteUser(productId) // 编译通过!运行时BUG!

// ✅ 安全的新代码
function deleteUserSafe(id: UserId) { ... }

const uId = UserId.create('user_001');
const pId = ProductId.create('prod_101');

// ✅ 正确调用
deleteUserSafe(uId);

// ❌ 报错:类型 'ProductId' 不能赋值给类型 'UserId'
deleteUserSafe(pId);

3. 进阶:不只是 ID,还有值对象

在 DDD(领域驱动设计)中,我们不仅区分 ID,还区分值对象 。比如"金额"和"数量"虽然都是 number,但绝不能混用。

typescript 复制代码
// 区分"金额"和"数量"
export type Money = Brand<number, 'Money'>;
export type Quantity = Brand<number, 'Quantity'>;

function calculateTotal(price: Money, qty: Quantity): Money {
    return (price * qty) as Money;
}

const price = 100 as Money;
const count = 5 as Quantity;

// ✅ 正确
const total = calculateTotal(price, count);

// ❌ 报错:防止把数量当成金额传进去
// calculateTotal(count, price); 

4. 运行时行为

因为品牌类型本质上是 T & { readonly __brand: B },而这个 __brand 属性在编译后会被完全擦除。

这意味着:

  • 零运行时开销 :最终生成的 JS 代码里,UserId 就是普通的 string
  • 序列化友好 :你可以安全地把 UserId 发送给后端(后端收到的是字符串),或者在 LocalStorage 里存它。

❓ 常见问题

Q1: type A = stringtype UserId = string & { __brand: 'User' } 生成的 JS 代码一样吗?

A: 是的,完全一样。 __brand 只是一个编译时的类型标记,不会出现在最终的 JS 代码中。这使得品牌类型既安全,又没有任何性能负担。

Q2: 如果后端返回的是普通字符串,怎么转成 UserId

A: 你需要有一个"边界转换层"。在 API 请求函数中,拿到数据后,使用工厂函数进行转换。

typescript 复制代码
const rawUser = await fetchUser();
// ⚠️ 不要在后端传 JSON 数据里带类型信息,在前端清洗
const safeUser = {
    ...rawUser,
    id: UserId.create(rawUser.id) // 类型清洗
};

Q3: 写 as string 可以把品牌类型去掉吗?

A: 可以。类型系统是防君子不防小人的。如果你刻意要用 id as string 强行绕过检查,TS 拦不住你。但在正常的业务流程中,这种强转应该被 Code Review 禁止。


🎯 总结

品牌类型是 TypeScript 赋予开发者的"业务级安全带"。本文我们掌握了:

  1. 原理 :利用交叉类型 T & { __brand: B } 模拟名义类型。
  2. 实践:通过工厂函数创建 ID 和值对象,杜绝"张冠李戴"。
  3. DDD 结合:用类型系统表达业务领域的概念,而不仅仅是技术类型。

🚀 下期预告:

我们学习了类型、模块、全栈、品牌类型,现在是时候把这些知识融合在一起了。

下一篇文章将是本系列的收官之作------《终极实战:构建一个类型安全的 UI 组件库》,我们将挑战多态组件和泛型的高级设计。

💬 互动环节:

你的项目里出现过 ID 混用导致的 BUG 吗?看完本文是不是想给所有的 ID 加上"标签"?

如果觉得文章让你的代码更健壮了,请点赞👍、收藏⭐、关注👀,最后一篇千万别错过!

相关推荐
xy345344 分钟前
Axure 9.0 中继器核心结构
前端·ui·html·axure·原型·产品设计
老王以为1 小时前
走进 AI Agent 第四篇(上):知识获取管道——RAG 基础
前端·人工智能·全栈
夏天要喝冰可乐1 小时前
一处写作,多平台分发:我给掘金做了一款开源 Chrome 插件
前端·chrome·vibecoding
货拉拉技术1 小时前
货拉拉开源Hadice:安卓 & 鸿蒙桌面调试工具
前端
程序猿DD2 小时前
OctaFuse Gateway 2.11.0:流式请求优化、用户折扣策略与错误契约升级
前端·后端
尾善爱看海2 小时前
《JavaScript 数组操作全攻略:12 类 API + 30 个实战场景 + 20 个避坑指南》
前端·javascript·面试
计算机魔术师2 小时前
Anthropic 估值冲 4 万亿,但企业客户已经偷偷换了便宜模型
前端
王六米。2 小时前
RAG知识库建设 文档解析 切分与索引如何衔接
服务器·前端·网络·企业数字化转型·智钳智能盒子
名字还没想好☜2 小时前
React 文件上传实战:预览 URL 回收、多文件逐个进度与拖拽放置区踩坑
前端·react·next.js