← 中文资源

简体中文版本

用品牌类型承载已校验的基础值

把运行时解析器与 TypeScript 品牌类型结合,让已校验的邮箱、用户 ID 与 slug 在编译期不可混用。

阅读英文原文

功能说明

该示例用品牌类型区分结构相同但语义不同的字符串。普通 string 既可能是邮箱也可能是用户 ID,函数参数一旦写反,编译器无法发现。通过给校验后的值附加唯一符号标记,Email、UserId 与 Slug 成为互不兼容的类型,同时运行时仍然是普通字符串。

关键代码

declare const brand: unique symbol 声明一个只存在于类型中的标记,Brand 用交叉类型把值与标记组合。parseEmail、parseUserId 与 parseSlug 分别做 trim、大小写归一与正则校验,只有通过后才用 as 断言成品牌类型。isEmail 是类型谓词,可以在 if 分支中把 string 收窄为 Email。sendWelcome 的参数使用品牌类型,误传普通字符串会直接编译失败。

使用注意

品牌在运行时会被完全擦除,JSON 反序列化后仍然是普通字符串,必须在边界处重新 parse。不要在校验函数之外使用 as 强制转换,否则品牌就失去意义。品牌类型不能替代服务端校验,客户端校验只改善体验。错误信息中不要回显完整敏感值。该模式适合小型基础类型,复杂对象应结合 schema 校验库。

验证方式

断言合法邮箱被归一为小写、合法用户 ID 与 slug 原样返回,非法输入分别抛错。用 isEmail 在分支中访问 Email 类型,确认类型收窄生效。编写一个只接受 Email 的函数并故意传入普通 string,确认 tsc 报错;传入 parseEmail 的结果则通过。

代码示例

declare const brand: unique symbol;

export type Brand<Value, Name extends string> = Value & { readonly [brand]: Name };
export type Email = Brand<string, "Email">;
export type UserId = Brand<string, "UserId">;
export type Slug = Brand<string, "Slug">;

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
const USER_ID_PATTERN = /^usr_[a-z0-9]{8,}$/;
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

export function parseEmail(value: string): Email {
  if (typeof value !== "string") throw new TypeError("email must be a string");
  const normalized = value.trim().toLowerCase();
  if (normalized.length > 254 || !EMAIL_PATTERN.test(normalized)) {
    throw new Error(`invalid email address "${value}"`);
  }
  return normalized as Email;
}

export function parseUserId(value: string): UserId {
  if (typeof value !== "string") throw new TypeError("user id must be a string");
  const normalized = value.trim();
  if (!USER_ID_PATTERN.test(normalized)) throw new Error(`invalid user id "${value}"`);
  return normalized as UserId;
}

export function parseSlug(value: string): Slug {
  if (typeof value !== "string") throw new TypeError("slug must be a string");
  const normalized = value.trim().toLowerCase();
  if (normalized.length === 0 || normalized.length > 80 || !SLUG_PATTERN.test(normalized)) {
    throw new Error(`invalid slug "${value}"`);
  }
  return normalized as Slug;
}

export function isEmail(value: string): value is Email {
  return value.length <= 254 && EMAIL_PATTERN.test(value.trim().toLowerCase());
}

function sendWelcome(email: Email, userId: UserId): string {
  return `queued welcome for ${userId} at ${email}`;
}

const email = parseEmail("  [email protected] ");
const userId = parseUserId("usr_1234abcd");
console.log(email, userId);
console.log(isEmail("not-an-email"));
console.log(sendWelcome(email, userId));
console.log(parseSlug("release-notes-2026"));