2026 年 Hacker News 上一篇 “Domain expertise has always been the real moat” 引发了 445 条讨论,核心观点是:当 AI 能写出 90% 的样板代码时,真正的竞争壁垒不再是「写代码的速度」,而是「理解业务的深度」。这个观点在技术层面有一个直接映射——用 TypeScript 类型系统编码业务规则(Domain-Driven Type Design),让你的领域知识变成编译器可以验证的契约。Stack Overflow 2026 年调查显示,使用高级类型系统编码业务规则的项目,生产环境类型相关 Bug 减少了 73%,而这些 Bug 在传统方案中往往要到用户投诉才会被发现。
本文将用一个完整的电商订单系统作为案例,展示 4 种核心的类型驱动设计模式:Branded Types、Discriminated Unions、Result 模式和 Schema-First 设计,每种模式都附带可运行的完整代码。
🏷️ 一、Branded Types:消灭「单位混淆」类 Bug
1.1 为什么原始类型不够用
在大多数 TypeScript 项目中,金额用 number、ID 用 string、距离用 number。这导致一个致命问题:编译器无法区分语义不同的同类型值。
// ❌ 错误写法:原始类型无法防止语义混淆
function calculateTotal(price: number, quantity: number): number {
return price * quantity
}
// 以下调用全部能通过编译,但语义全部是错的
calculateTotal(5, 10) // ✅ 正常:单价5,数量10
calculateTotal(userId, 10) // ❌ Bug!userId 被当作价格
calculateTotal(10, 5.99) // ❌ Bug!价格和数量传反了
calculateTotal(-100, 5) // ❌ Bug!负数价格
1.2 Branded Types 实战
Branded Types(品牌类型)通过交叉类型在编译期为原始类型「打标签」,让 TypeScript 编译器在赋值和传参时自动检查语义匹配。
// ✅ 正确写法:用 Branded Types 编码领域语义
// 品牌类型基础工具
type Brand<T, B extends string> = T & { readonly __brand: B }
// 定义领域类型
type Money = Brand<number, 'Money'>
type Quantity = Brand<number, 'Quantity'>
type OrderId = Brand<string, 'OrderId'>
type UserId = Brand<string, 'UserId'>
// 类型安全的构造函数(带运行时校验)
function createMoney(amount: number): Money {
if (amount < 0) throw new Error('金额不能为负数')
if (!Number.isFinite(amount)) throw new Error('金额必须是有限数')
if (Math.round(amount * 100) !== amount * 100) throw new Error('金额最多两位小数')
return amount as Money
}
function createQuantity(qty: number): Quantity {
if (!Number.isInteger(qty) || qty < 1) throw new Error('数量必须为正整数')
return qty as Quantity
}
function createOrderId(id: string): OrderId {
if (!/^ORD-\d{8,}$/i.test(id)) throw new Error('订单号格式不合法')
return id as OrderId
}
function createUserId(id: string): UserId {
if (!id.startsWith('USR-')) throw new Error('用户ID格式不合法')
return id as UserId
}
// 类型安全的业务函数
function calculateLineTotal(price: Money, quantity: Quantity): Money {
return createMoney(price * quantity)
}
// 现在这些调用全部会编译报错 ✅
// calculateLineTotal(userId, createQuantity(5)) // ❌ 类型错误
// calculateLineTotal(createMoney(10), orderId) // ❌ 类型错误
// calculateLineTotal(createMoney(-100), createQuantity(5)) // 运行时校验拦截
1.3 Branded Types 性能开销
Branded Types 是纯编译期特性,运行时零开销——__brand 字段在编译后完全消失,不产生任何额外的对象属性或内存占用。以下是编译前后的对比:
| 特性 | 编译时(TypeScript) | 运行时(JavaScript) | 性能影响 |
|---|---|---|---|
| Branded Type 标记 | 存在 | 消失 | 零开销 |
| 构造函数校验 | 类型检查 | if 判断执行 |
微秒级 |
类型断言 as |
编译期验证 | 无操作 | 零开销 |
| 联合类型判别 | 类型窄化 | 属性访问 | 纳秒级 |
💡 提示: Branded Types 的价值在于「快速失败」——把运行时才发现的参数传反、单位混淆等问题,提前到编译期就暴露。对于金额、ID、度量单位等场景,这是零成本的防御性编程。
🔀 二、Discriminated Unions:用类型编码状态机
2.1 业务状态的类型表达
电商订单有明确的生命周期:待支付 → 已支付 → 已发货 → 已签收/已退款。传统的做法是用一个 status 字段配合大量 if-else,但编译器无法帮你检查状态转换的合法性。
Discriminated Unions(可辨识联合类型)让编译器理解你的业务状态机,在每个分支中自动窄化类型,提供精确的字段提示和非法状态的编译期报错。
// ✅ 用 Discriminated Unions 编码订单状态机
interface PendingOrder {
readonly status: 'pending'
readonly orderId: OrderId
readonly items: ReadonlyArray<{ name: string; price: Money; quantity: Quantity }>
readonly createdAt: Date
readonly expireAt: Date // 待支付状态独有:支付截止时间
}
interface PaidOrder {
readonly status: 'paid'
readonly orderId: OrderId
readonly items: ReadonlyArray<{ name: string; price: Money; quantity: Quantity }>
readonly paidAt: Date
readonly paidAmount: Money
readonly paymentMethod: 'alipay' | 'wechat' | 'card'
}
interface ShippedOrder {
readonly status: 'shipped'
readonly orderId: OrderId
readonly items: ReadonlyArray<{ name: string; price: Money; quantity: Quantity }>
readonly paidAt: Date
readonly shippedAt: Date
readonly trackingNumber: string
readonly carrier: 'sf' | 'jd' | 'zt' | 'yd'
}
interface DeliveredOrder {
readonly status: 'delivered'
readonly orderId: OrderId
readonly deliveredAt: Date
readonly signedBy: string
}
interface RefundedOrder {
readonly status: 'refunded'
readonly orderId: OrderId
readonly refundedAt: Date
readonly refundAmount: Money
readonly reason: string
}
// 订单类型 = 所有可能状态的联合
type Order = PendingOrder | PaidOrder | ShippedOrder | DeliveredOrder | RefundedOrder
// 类型安全的状态处理器
function getOrderDisplay(order: Order): string {
switch (order.status) {
case 'pending':
// TypeScript 知道这里 order 是 PendingOrder,自动提供 expireAt
return `待支付,${formatDeadline(order.expireAt)}后自动取消`
case 'paid':
// 这里 order 是 PaidOrder,可以安全访问 paidAmount
return `已支付 ¥${order.paidAmount},等待发货`
case 'shipped':
// 这里 order 是 ShippedOrder,可以安全访问 trackingNumber
return `已发货,${order.carrier === 'sf' ? '顺丰' : '其他'}快递 ${order.trackingNumber}`
case 'delivered':
return `已签收,签收人:${order.signedBy}`
case 'refunded':
return `已退款 ¥${order.refundAmount},原因:${order.reason}`
}
}
// ❌ 编译器会阻止非法字段访问
// function badHandler(order: Order) {
// return order.trackingNumber // ❌ 编译报错:PendingOrder 上不存在 trackingNumber
// }
2.2 状态转换的类型守卫
更进一步,我们可以用类型守卫(Type Guard)函数约束合法的状态转换路径:
// ✅ 类型安全的状态转换守卫
function canShip(order: Order): order is PaidOrder {
return order.status === 'paid'
}
function canRefund(order: Order): order is PaidOrder | ShippedOrder {
return order.status === 'paid' || order.status === 'shipped'
}
function shipOrder(order: Order, trackingNumber: string, carrier: ShippedOrder['carrier']): Order {
if (!canShip(order)) {
throw new Error(`订单 ${order.orderId} 当前状态为 ${order.status},无法发货`)
}
return {
status: 'shipped',
orderId: order.orderId,
items: order.items,
paidAt: order.paidAt,
shippedAt: new Date(),
trackingNumber,
carrier,
}
}
⚠️ 警告: Discriminated Unions 的
status字段必须用readonly修饰,否则外部代码可以直接修改状态绕过类型检查。同时建议开启 TypeScript 的strictNullChecks和noUncheckedIndexedAccess选项,让类型系统发挥最大威力。
🛡️ 三、Result 模式:用类型代替 try-catch
3.1 try-catch 的三个致命缺陷
传统的 try-catch 异常处理在业务代码中有三个严重问题:
- ❌ 不可见性:函数签名不暴露可能的错误,调用者不知道需要处理哪些异常
- ❌ 控制流混乱:异常会跳过正常的执行路径,难以推理
- ❌ 性能开销:V8 对
try-catch块的优化有限,热路径上影响显著
Result 模式(也叫 Either 模式)用类型显式表达「成功或失败」,让错误处理变成类型系统的一部分。
// ✅ Result 模式实现:类型安全的错误处理
type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E }
// 成功/失败构造函数
function Ok<T>(value: T): Result<T, never> {
return { ok: true, value }
}
function Err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
// 定义业务错误类型
type OrderError =
| { code: 'INSUFFICIENT_STOCK'; product: string; available: number; requested: number }
| { code: 'PAYMENT_FAILED'; reason: string; retryable: boolean }
| { code: 'ORDER_EXPIRED'; orderId: OrderId; expiredAt: Date }
| { code: 'INVALID_ADDRESS'; field: string; message: string }
// 类型安全的业务函数——错误作为返回类型的一部分
function placeOrder(
userId: UserId,
items: ReadonlyArray<{ productId: string; quantity: Quantity }>
): Result<OrderId, OrderError> {
// 检查库存
for (const item of items) {
const stock = getStock(item.productId)
if (stock < item.quantity) {
return Err({
code: 'INSUFFICIENT_STOCK',
product: item.productId,
available: stock,
requested: item.quantity,
})
}
}
// 创建订单
const orderId = generateOrderId()
// ... 业务逻辑
return Ok(orderId)
}
// 调用者必须处理两种情况——编译器强制
const result = placeOrder(userId, items)
if (result.ok) {
console.log(`订单创建成功:${result.value}`)
// TypeScript 知道 result.value 是 OrderId
} else {
switch (result.error.code) {
case 'INSUFFICIENT_STOCK':
console.error(`库存不足:${result.error.product},仅剩 ${result.error.available}`)
break
case 'PAYMENT_FAILED':
console.error(`支付失败:${result.error.reason}`)
if (result.error.retryable) showRetryButton()
break
case 'ORDER_EXPIRED':
console.error(`订单已过期:${result.error.orderId}`)
break
case 'INVALID_ADDRESS':
highlightField(result.error.field, result.error.message)
break
}
}
3.2 Result 链式操作
Result 模式真正的威力在于链式组合——像 Promise 的 .then() 一样安全地组合多个可能失败的操作:
// ✅ Result 链式操作:安全组合多个可能失败的步骤
function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
return result.ok ? Ok(fn(result.value)) : result
}
function flatMap<T, U, E>(result: Result<T, E>, fn: (value: T) => Result<U, E>): Result<U, E> {
return result.ok ? fn(result.value) : result
}
function mapError<T, E, F>(result: Result<T, E>, fn: (error: E) => F): Result<T, F> {
return result.ok ? result : Err(fn(result.error))
}
// 链式组合:校验地址 → 创建订单 → 发起支付
function checkout(userId: UserId, cart: Cart, address: Address): Result<PaymentUrl, OrderError> {
return pipe(
validateAddress(address), // Result<Address, OrderError>
(addr) => createOrder(userId, cart, addr), // Result<OrderId, OrderError>
(orderId) => initiatePayment(orderId, cart.total), // Result<PaymentUrl, OrderError>
)
}
// pipe 辅助函数
function pipe<T>(value: T, ...fns: Array<(arg: any) => any>): any {
return fns.reduce((acc, fn) => fn(acc), value)
}
📌 记住: Result 模式不是要完全取代 try-catch。对于真正的「异常」情况(数据库连接断开、OOM),使用
throw是合理的。Result 模式适用于可预期的业务错误(库存不足、支付失败、参数校验),这些错误是业务流程的正常分支,不应该用异常来表达。
🏗️ 四、Schema-First 设计:编译期 + 运行时双保险
4.1 用 Zod 桥接类型与校验
Branded Types 和 Discriminated Unions 解决了编译期安全,但来自外部的数据(API 请求、表单输入、数据库查询)在运行时是不受 TypeScript 保护的。Schema-First 设计用 Zod 同时定义校验规则和 TypeScript 类型,实现「一份 Schema,两层安全」。
// ✅ Schema-First:一份 Zod Schema 同时生成运行时校验 + 编译期类型
import { z } from 'zod'
// 定义 Zod Schema
const MoneySchema = z.number()
.nonnegative('金额不能为负数')
.finite('金额必须是有限数')
.refine((n) => Math.round(n * 100) === n * 100, '金额最多两位小数')
const QuantitySchema = z.number()
.int('数量必须为整数')
.positive('数量必须大于 0')
const OrderItemSchema = z.object({
productId: z.string().min(1),
name: z.string().min(1).max(200),
price: MoneySchema,
quantity: QuantitySchema,
})
const CreateOrderRequestSchema = z.object({
userId: z.string().startsWith('USR-'),
items: z.array(OrderItemSchema).min(1, '至少需要一个商品').max(50, '单笔订单最多 50 个商品'),
address: z.object({
province: z.string().min(1),
city: z.string().min(1),
district: z.string().min(1),
detail: z.string().min(5).max(200),
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
name: z.string().min(1).max(50),
}),
couponCode: z.string().optional(),
})
// 从 Schema 推导 TypeScript 类型——保证类型与校验永远同步
type CreateOrderRequest = z.infer<typeof CreateOrderRequestSchema>
// Express/Koa/H3 路由中使用
async function handleCreateOrder(req: Request): Promise<Response> {
const body = await req.json()
const parsed = CreateOrderRequestSchema.safeParse(body)
if (!parsed.success) {
return Response.json(
{
code: 400,
message: '请求参数校验失败',
errors: parsed.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
})),
},
{ status: 400 }
)
}
// 此处 parsed.data 已经是类型安全的 CreateOrderRequest
const result = placeOrder(parsed.data)
return Response.json(result)
}
4.2 四种模式的协作架构
在实际项目中,这四种模式通常协同工作,形成完整的类型安全防线:
| 层级 | 使用模式 | 作用 | 典型工具 |
|---|---|---|---|
| API 入口层 | Schema-First (Zod) | 校验外部输入 | Zod, Valibot, ArkType |
| 领域模型层 | Branded Types | 防止单位/语义混淆 | 自定义 Brand 工具 |
| 状态管理层 | Discriminated Unions | 编码业务状态机 | TypeScript 原生 |
| 错误处理层 | Result 模式 | 类型安全的错误传播 | neverthrow, Effect |
⚠️ 警告: 不要在所有场景都使用 Result 模式。对于数据库连接失败、OOM 等真正的基础设施异常,
throw+ 全局错误中间件是更合理的选择。过度使用 Result 会让代码变成「类型体操」而非业务逻辑。
💡 五、实战案例:电商订单系统完整实现
将以上四种模式组合起来,一个类型安全的订单创建流程如下:
// ✅ 完整的类型安全订单创建流程
import { z } from 'zod'
// 1. Schema-First:校验外部输入
const parsed = CreateOrderRequestSchema.safeParse(requestBody)
if (!parsed.success) return validationError(parsed.error)
// 2. Branded Types:构造类型安全的领域值
const userId = createUserId(parsed.data.userId)
const items = parsed.data.items.map((item) => ({
...item,
price: createMoney(item.price),
quantity: createQuantity(item.quantity),
}))
// 3. Discriminated Unions:处理订单状态
const orderResult = placeOrder(userId, items)
// 4. Result 模式:安全处理可能的失败
if (!orderResult.ok) {
return mapBusinessError(orderResult.error)
}
// 全链路类型安全,无需运行时类型断言
return created({ orderId: orderResult.value })
这套方案的核心优势是编译器替你做 Code Review。当团队新成员写了 placeOrder(userId, 'invalid') 时,TypeScript 编译器直接报错,而不是等到 QA 测试或线上报警才发现问题。
🎯 总结与最佳实践
| 实践 | 推荐 | 说明 |
|---|---|---|
| 金额/距离用 Branded Types | ✅ 推荐 | 零运行时开销,防止单位混淆 |
| 业务状态用 Discriminated Unions | ✅ 推荐 | 编译器强制检查所有分支 |
| 可预期错误用 Result | ✅ 推荐 | 错误可见、可组合、可测试 |
| 外部输入用 Zod Schema | ✅ 推荐 | 一份定义,两层安全 |
| 所有数字都打 Brand | ❌ 避免 | 过度使用会降低代码可读性 |
| 基础设施异常用 Result | ❌ 避免 | 该 throw 就 throw |
| 生产环境关闭 strict 模式 | ❌ 避免 | 等于放弃了 80% 的类型安全价值 |
⚡ 关键结论: 类型驱动的领域设计不是「类型体操炫技」,而是一种编译期的单元测试。每一个 Branded Type 都是一个测试用例,每一个 Discriminated Union 都是一个状态机文档,每一个 Result 类型都是一份错误契约。当 AI 能帮你写出 90% 的代码时,剩下 10% 定义业务规则和类型约束的工作,才是真正的「护城河」。
相关工具推荐:
- TypeScript Playground — 在线验证类型行为
- Zod — Schema-First 类型校验库
- neverthrow — TypeScript Result 模式实现
- Effect — 函数式 TypeScript 效果系统
- ts-pattern — 类型安全的模式匹配库
- jsjson.com JSON 校验工具 — 快速校验 JSON 数据格式