← 返回题目列表

TypeScript 品牌类型是什么?如何避免相同基础类型混用?

中等 第 26 / 32 题 更新于 2026/07/29
TypeScript品牌类型类型建模

简化版

品牌类型是在基础类型上叠加一个只用于类型区分的标记,让同样都是 stringnumber 的值不能随便混用。

它常用于用户 ID、订单 ID、金额分、邮箱、URL 等业务语义不同但底层类型相同的值。品牌类型主要提供编译期保护,运行时仍然是原来的值。

详细版

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

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function getUser(id: UserId) {}

const orderId = "o1" as OrderId;
getUser(orderId); // 类型错误

通常配合构造函数或校验函数生成品牌值:

function createUserId(value: string): UserId {
  if (!value.startsWith("u_")) throw new Error("invalid user id");
  return value as UserId;
}

不要到处 as UserId,否则品牌类型会失去保护意义。

完整版教学

一、为什么结构化类型需要品牌

TypeScript 是结构化类型系统,只要结构相同就兼容。

type UserId = string;
type OrderId = string;

这两个类型本质上都是 string,不能阻止混用。

品牌类型是在结构化类型系统里人为加入“名义类型”的味道。

二、基础写法

type Brand<T, Name extends string> = T & {
  readonly __brand: Name;
};

然后定义:

type UserId = Brand<string, "UserId">;
type ProductId = Brand<string, "ProductId">;
类型运行时值编译期语义
string字符串普通文本
UserId字符串用户 ID
ProductId字符串商品 ID

三、品牌字段通常不真实存在

品牌字段只是类型层标记。

const id = "u_1" as UserId;
console.log(id); // "u_1"

运行时没有额外对象包装,也没有 __brand 字段。

这意味着品牌类型不能替代运行时校验。

四、用工厂函数集中创建

function toUserId(value: string): UserId {
  if (!/^u_\d+$/.test(value)) {
    throw new Error("invalid user id");
  }
  return value as UserId;
}

集中创建有两个好处:

  • 校验逻辑统一
  • as 被限制在边界位置

五、适合哪些业务值

常见场景:

  • UserIdOrderId
  • Cent 和普通 number
  • Email 和普通 string
  • SafeHtml 和普通 string
  • AbsoluteUrl 和普通 string
type Cent = Brand<number, "Cent">;

数字例子:100 可以表示 100 元、100 分、100 个库存,品牌类型能让含义更明确。

六、品牌类型的边界

品牌类型是编译期约束,不会阻止外部 JSON 传错值。

从接口、localStorage、URL 参数拿到的数据仍然要校验。

const raw = JSON.parse(text) as unknown;

不要直接把未知数据断言成品牌类型,应通过解析函数转换。

七、常见误区与追问

  • 误区:品牌类型会改变运行时数据结构。 它通常只存在于编译期。
  • 误区:到处 as Brand 也很安全。 随意断言会绕过校验,品牌失去意义。
  • 误区:品牌类型只适合字符串。 number、对象引用等也可以加品牌。
  • 误区:品牌类型能替代后端校验。 它不能验证外部输入的真实性。
  • 追问:为什么 TypeScript 需要品牌类型? 因为结构化类型无法区分相同结构的不同业务语义。
  • 追问:品牌字段为什么常用 readonly? 避免被业务代码当成可写数据字段。

八、加强记忆

记忆链路:

raw string -> validate -> branded string -> domain API

回答时先讲结构化类型的混用问题,再给 UserId/OrderId 例子,最后强调品牌类型只负责编译期隔离,运行时仍要校验。