Kodokon kodokon.com

生产模式:品牌类型、satisfies 与 .d.ts

运用能保护真实代码库的专家级模式:模拟的名义性、不发生放宽的校验,以及环境声明。

11 分钟 · 3 题

在 Kodokon 中打开本课

TypeScript 的类型是结构化的:两个形状相同的类型可以互换,所以一个用户 id 的 string 与任何其他 string 无法区分。品牌化(Branding)通过把基础类型与一个幽灵属性求交叉来模拟名义类型,该属性的键是一个 unique symbol:没有任何真实的值会拥有它,但编译器现在能够区分每一种品牌。

TYPESCRIPT
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 rejected
模拟的名义性,运行时零成本。

satisfies 运算符(TypeScript 4.9)填补了一个精确的空白。: T 标注会把变量的类型放宽T 并丢失推断出的字面量;as T 会禁用一部分检查。satisfies T不触动推断类型的前提下验证表达式是否符合 T,推断类型保持尽可能精确。它是配置对象的理想工具:完整校验,精度不减。

TYPESCRIPT
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
satisfies:严格检查而不发生放宽。

一个 .d.ts 文件只包含环境声明:它描述那些将在运行时存在的值,而不生成一行 JavaScript。要从一个模块扩展全局作用域,declare global 块是必需的 - 而规范只允许它出现在一个确实是模块的文件中,因此文件顶部要用 export {} 这个惯用写法。

TYPESCRIPT
export {};

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}
env.d.ts:export {} 把该文件变成一个模块。

对于一个没有发布类型的 JavaScript 依赖,declare module 会创建一个环境模块:编译器会在该包的每一次导入中使用你的声明。要小心 skipLibCheck 的作用范围:启用时,它会忽略所有 .d.ts 文件的错误,包括你自己的;所以要通过放在普通 .ts 文件中的类型测试来验证你的声明。

TYPESCRIPT
declare module "legacy-lib" {
  export interface InitOptions {
    debug?: boolean;
  }
  export function init(options?: InitOptions): void;
}
legacy-lib.d.ts:为一个没有发布类型的包建立类型。

知识检测

确认你已牢记本课的重点内容。

  1. satisfies T: T 标注之间有什么区别?
    • satisfies 把表达式的类型放宽到 T
    • satisfies 检查是否符合 T,同时保留更精确的推断类型
    • satisfies 像 as 一样禁用检查
    • 没有区别:两种形式等价
  2. 在运行时,一个用 Brand<string, ...> 构建的品牌值包含什么?
    • 一个字符串加上一个 symbol 属性
    • 一个普通的字符串:品牌只在编译期存在
    • 一个包裹了原始字符串的对象
  3. 为什么要在使用 declare global 的 .d.ts 文件顶部加上 export {}
    • 为了把全局类型导出到其他文件
    • 为了把该文件变成一个模块,这是 declare global 要求的条件
    • 为了自动启用严格模式
    • 它纯粹是装饰性的