运用能保护真实代码库的专家级模式:模拟的名义性、不发生放宽的校验,以及环境声明。
在 Kodokon 中打开本课TypeScript 的类型是结构化的:两个形状相同的类型可以互换,所以一个用户 id 的 string 与任何其他 string 无法区分。品牌化(Branding)通过把基础类型与一个幽灵属性求交叉来模拟名义类型,该属性的键是一个 unique symbol:没有任何真实的值会拥有它,但编译器现在能够区分每一种品牌。
declare const brand: unique symbol;
type Brand<T, Name extends string> =
T & { readonly [brand]: Name };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function asUserId(raw: string): UserId {
return raw as UserId;
}
declare function loadUser(id: UserId): void;
loadUser(asUserId("u_42")); // OK
// loadUser("u_42"); // error: bare string rejectedsatisfies 运算符(TypeScript 4.9)填补了一个精确的空白。: T 标注会把变量的类型放宽到 T 并丢失推断出的字面量;as T 会禁用一部分检查。satisfies T 在不触动推断类型的前提下验证表达式是否符合 T,推断类型保持尽可能精确。它是配置对象的理想工具:完整校验,精度不减。
type RouteDef = { path: string; auth?: boolean };
const routes = {
home: { path: "/" },
admin: { path: "/admin", auth: true },
} satisfies Record<string, RouteDef>;
routes.admin.auth;
// literal type true: the inferred precision is
// preserved, and an unknown key would be rejected一个 .d.ts 文件只包含环境声明:它描述那些将在运行时存在的值,而不生成一行 JavaScript。要从一个模块扩展全局作用域,declare global 块是必需的 - 而规范只允许它出现在一个确实是模块的文件中,因此文件顶部要用 export {} 这个惯用写法。
export {};
declare global {
interface Window {
analytics: { track(event: string): void };
}
}对于一个没有发布类型的 JavaScript 依赖,declare module 会创建一个环境模块:编译器会在该包的每一次导入中使用你的声明。要小心 skipLibCheck 的作用范围:启用时,它会忽略所有 .d.ts 文件的错误,包括你自己的;所以要通过放在普通 .ts 文件中的类型测试来验证你的声明。
declare module "legacy-lib" {
export interface InitOptions {
debug?: boolean;
}
export function init(options?: InitOptions): void;
}satisfies T 与 : T 标注之间有什么区别?Brand<string, ...> 构建的品牌值包含什么?declare global 的 .d.ts 文件顶部加上 export {}?