TypeScript 使用结构类型(鸭子类型):具有相同形状的两种类型可以互换。当基元类型返回域概念时,这会导致错误 - 在需要 orderId 的地方传递 userId ,两者都键入为 string 。考虑一个发送电子邮件但意外收到数据库 ID 而不是地址的函数,因为两者都是 string。品牌类型通过添加幻像类型标记来解决这个问题,该标记在编译时以零运行时成本区分结构相同的类型。
品牌格局
核心技术使用具有幻影品牌属性的交叉类型:
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
__brand 属性在运行时永远不存在——它在编译期间被删除。 TypeScript 将 UserId 和 OrderId 视为不同的类型,因为它们的品牌不同。
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
const uid = "user_123" as UserId;
const oid = "ord_456" as OrderId;
getUser(uid); // OK
getUser(oid); // Type error: 'OrderId' is not assignable to 'UserId'
使用 declare 或将品牌属性设置为可选以避免运行时分配问题:
// Better: unique symbol brand prevents accidental casting
declare const UserIdBrand: unique symbol;
type UserId = string & { [UserIdBrand]: true };
declare const OrderIdBrand: unique symbol;
type OrderId = string & { [OrderIdBrand]: true };
类型保护和验证函数
由于品牌在运行时不存在,因此必须在系统边界(API 端点、数据库结果和表单提交)对值进行验证和品牌化。
// Factory function with validation
function createUserId(value: string): UserId {
if (!/^user_[a-f0-9]{24}$/.test(value)) {
throw new Error(`Invalid UserId format: ${value}`);
}
return value as UserId;
}
// Type guard
function isUserId(value: string): value is UserId {
return /^user_[a-f0-9]{24}$/.test(value);
}
// Assertion function
function assertUserId(value: string): asserts value is UserId {
if (!isUserId(value)) {
throw new Error(`Expected a valid UserId, got: ${value}`);
}
}
为复杂类型编写验证器并处理反序列化:
interface User {
id: UserId;
email: Email;
name: string;
}
function parseUser(data: unknown): User {
const raw = data as Record<string, unknown>;
return {
id: createUserId(String(raw.id)),
email: createEmail(String(raw.email)),
name: String(raw.name),
};
}
域原语的用例
品牌类型在原始混乱造成真正损害的领域大放异彩。
| 域名 | 品牌类型 | 预防 |
|---|---|---|
| 电子商务 | UserId、OrderId、ProductId | 交叉 ID 分配错误 |
| 金融科技 | USD、JPY、EUR | 货币算术错误 |
| 医疗保健 | PatientId、ProviderId、ClaimId | 患者之间的数据泄露 |
| SaaS | ApiKey、SessionToken | 凭证滥用 |
货币处理是一个引人注目的例子:
type USD = Brand<number, "USD">;
type JPY = Brand<number, "JPY">;
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
function addJPY(a: JPY, b: JPY): JPY {
return (a + b) as JPY;
}
const price: USD = 29.99 as USD;
const tax: USD = 3.00 as USD;
const yenAmount: JPY = 5000 as JPY;
addUSD(price, tax); // OK
addUSD(price, yenAmount); // Type error: 'JPY' not assignable to 'USD'
通用品牌类型和库
为常见品牌模式创建通用实用程序:
type Entity<T extends string> = Brand<string, T>;
interface Repository<T, TId extends string> {
findById(id: Entity<TId>): Promise<T>;
save(entity: T): Promise<void>;
}
// Usage with Zod for runtime validation + branding
import { z } from "zod";
const UserIdSchema = z.string().brand("UserId");
type UserId = z.infer<typeof UserIdSchema>;
const userRepo: Repository<User, "UserId"> = {
async findById(id) {
// Type-safe database query
return db.query("SELECT * FROM users WHERE id = $1", [id]);
},
async save(user) { /* ... */ },
};
| 图书馆 | 品牌推广方法 | 运行时成本 |
|---|---|---|
| 占位符_0 | 占位符_1 | 零 |
| 占位符_0 | 品牌编解码器 | 仅验证 |
| 占位符_0 | .brand() 方法 | 仅验证 |
| 手册 | 交叉口类型 | 零 |
性能和迁移
品牌类型的运行时开销为零——品牌属性在编译期间被删除。验证函数确实会增加运行时成本,因此在系统边界验证一次并通过内部代码库传播品牌类型。
// Validate at boundary, propagate internally
async function handleRequest(rawId: string) {
const userId = createUserId(rawId); // Validate once
const user = await userRepo.findById(userId);
await sendEmail(user.email, welcomeMessage);
// userId and user.email are branded — type-safe throughout
}
增量迁移:从最关键的域原语(ID、货币金额)开始,在模块边界添加品牌类型,并使用 lint 规则强制执行。从新代码开始或一次重构一个模块,而不是一揽子迁移。
品牌类型提供零成本的编译时安全性,通过类型传达域含义来提高代码可读性,并防止整个类别的错误。安全方面的好处远远超过了冗长的代价。将它们集成到您的系统边界,并让类型检查器保护您的域逻辑。

