功能说明
该示例用品牌类型区分结构相同但语义不同的字符串。普通 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"));