You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何防止TypeScript混淆不同类型的数字ID?

问题原因

TypeScript 默认采用结构化类型系统,直接用 type XxxId = number 定义的类型只是 number 的别名,和原始 number 类型、其他同样指向 number 的别名完全等价,类型系统不会做区分。目前 TypeScript 没有内置配置项可以直接让同基础类型的别名互相不兼容,需要手动实现标称类型(Nominal Type)来达到类型区分的效果。

解决方案:使用品牌类型(Branded Type)

这是 TypeScript 社区通用的无运行时开销的标称类型实现方案,通过给基础类型交叉一个编译期唯一的「品牌标记」,让不同含义的ID类型在类型层面不兼容。

基础实现

首先定义通用的品牌类型工具:

type Brand<T, BrandName> = T & { __brand: BrandName }

基于这个工具改造你的ID类型定义即可:

type AId = Brand<number, 'A'>
type A = {id: AId}
type BId = Brand<number, 'B'>
type B = {id: BId}

const fnA = (id: AId) => {}

// 注意:原始number字面量不能直接赋值给品牌类型,需要通过类型断言或者专门的构造函数转换
const b: B = {id: 5 as BId}

// 此处TS会主动抛出类型错误,提示BId类型不能赋值给AId类型
fnA(b.id)

最佳实践

不要在业务代码里随处写类型断言转换ID,建议为每个ID类型写专门的构造函数,必要时可以在构造函数里加运行时校验:

const createAId = (val: number): AId => {
  // 可选:加运行时校验,比如ID范围、格式检查
  if (!Number.isInteger(val) || val <= 0) {
    throw new Error('invalid AId')
  }
  return val as AId
}

const createBId = (val: number): BId => val as BId

// 业务里统一用构造函数生成ID,从源头避免类型混传
const aId = createAId(1)
const bId = createBId(5)
fnA(aId) // 合法
fnA(bId) // 类型错误

大型项目优化

如果是多模块的大型项目,可以用 unique symbol 作为品牌标记的key,避免不同模块同名品牌名冲突:

declare const AIdBrand: unique symbol
type AId = number & { [AIdBrand]: never }

declare const BIdBrand: unique symbol
type BId = number & { [BIdBrand]: never }

这种写法的品牌标记是全局唯一的,不需要额外传品牌名,也不会出现命名冲突。

补充说明

品牌标记仅在类型编译阶段存在,编译为JavaScript后会被完全擦除,不会产生任何额外的运行时开销。

内容的提问来源于stack exchange,提问作者w00t

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 07:48:32