> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# TypeScript 最佳实践
- URL: https://blog.vercanti.com/typescript-zui-jia-shi-jian/
- Published: 2026-08-28T14:35:22.000Z
- Updated: 2026-08-28T14:58:41.000Z
- Description: 相关文档：TypeScript完全指南(/typescript-wan-quan-zhi-nan/) | JS模块系统(/js-mjs-ts-wen-jian-qu-bie-yu-zui-jia-shi-jian/) | JavaScript Promise 完全指南(/javascript-promise-wan-quan-zhi-nan/) 1. 概述(#%E6%A6%82%E8%BF%B0) 2. 编译配置：把检查开到最严(#%E4%B8%80%E3%80%81%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE%EF%BC%9
- Author: yellowdog
- Tags: 前端开发

> 官方文档：<https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html>  
> tsconfig 参考：<https://www.typescriptlang.org/tsconfig>  
> 适用版本：TypeScript 5.4+（2026-05-23 核实）

相关文档：[TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/) | [JS模块系统](https://blog.vercanti.com/js-mjs-ts-wen-jian-qu-bie-yu-zui-jia-shi-jian/) | [JavaScript Promise 完全指南](https://blog.vercanti.com/javascript-promise-wan-quan-zhi-nan/)

---

## 目录

1. [概述](#%E6%A6%82%E8%BF%B0)
2. [编译配置：把检查开到最严](#%E4%B8%80%E3%80%81%E7%BC%96%E8%AF%91%E9%85%8D%E7%BD%AE%EF%BC%9A%E6%8A%8A%E6%A3%80%E6%9F%A5%E5%BC%80%E5%88%B0%E6%9C%80%E4%B8%A5)
3. [类型设计：让类型表达意图](#%E4%BA%8C%E3%80%81%E7%B1%BB%E5%9E%8B%E8%AE%BE%E8%AE%A1%EF%BC%9A%E8%AE%A9%E7%B1%BB%E5%9E%8B%E8%A1%A8%E8%BE%BE%E6%84%8F%E5%9B%BE)
4. [函数与 API 设计](#%E4%B8%89%E3%80%81%E5%87%BD%E6%95%B0%E4%B8%8E-api-%E8%AE%BE%E8%AE%A1)
5. [泛型：约束优先于自由](#%E5%9B%9B%E3%80%81%E6%B3%9B%E5%9E%8B%EF%BC%9A%E7%BA%A6%E6%9D%9F%E4%BC%98%E5%85%88%E4%BA%8E%E8%87%AA%E7%94%B1)
6. [模块与类型导入](#%E4%BA%94%E3%80%81%E6%A8%A1%E5%9D%97%E4%B8%8E%E7%B1%BB%E5%9E%8B%E5%AF%BC%E5%85%A5)
7. [空值与防御性编程](#%E5%85%AD%E3%80%81%E7%A9%BA%E5%80%BC%E4%B8%8E%E9%98%B2%E5%BE%A1%E6%80%A7%E7%BC%96%E7%A8%8B)
8. [错误处理](#%E4%B8%83%E3%80%81%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86)
9. [与第三方库协作](#%E5%85%AB%E3%80%81%E4%B8%8E%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93%E5%8D%8F%E4%BD%9C)
10. [性能与构建](#%E4%B9%9D%E3%80%81%E6%80%A7%E8%83%BD%E4%B8%8E%E6%9E%84%E5%BB%BA)
11. [代码组织与命名](#%E5%8D%81%E3%80%81%E4%BB%A3%E7%A0%81%E7%BB%84%E7%BB%87%E4%B8%8E%E5%91%BD%E5%90%8D)
12. [语法选择决策：多用什么、少用什么、何时用什么](#%E5%8D%81%E4%B8%80%E3%80%81%E8%AF%AD%E6%B3%95%E9%80%89%E6%8B%A9%E5%86%B3%E7%AD%96%EF%BC%9A%E5%A4%9A%E7%94%A8%E4%BB%80%E4%B9%88%E3%80%81%E5%B0%91%E7%94%A8%E4%BB%80%E4%B9%88%E3%80%81%E4%BD%95%E6%97%B6%E7%94%A8%E4%BB%80%E4%B9%88)
13. [类型层面测试](#%E5%8D%81%E4%BA%8C%E3%80%81%E7%B1%BB%E5%9E%8B%E5%B1%82%E9%9D%A2%E6%B5%8B%E8%AF%95)
14. [常见陷阱](#%E5%8D%81%E4%B8%89%E3%80%81%E5%B8%B8%E8%A7%81%E9%99%B7%E9%98%B1)
15. [参见](#%E5%8F%82%E8%A7%81)

---

## 概述

**What**：本文不重复 TypeScript 语法手册（参见 [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)），而是回答"在真实项目里，怎样的 TS 代码才算高质量"——围绕类型设计、API 设计、错误处理、性能、与生态协作整理 35 条可执行实践。

**Why**：TypeScript 的类型系统极其强大，但默认配置宽松、`any` 兜底容易让团队退化成"加了类型注释的 JavaScript"。Flow 与 Sound TypeScript 的对比、TS 5.x 引入的 `satisfies` 与 `verbatimModuleSyntax`，使得 2024 年后写 TS 的方式已与 2020 年明显不同。

**When**：适用于 ≥ 3 人协作的中大型项目、需要长期维护的库、希望减少运行时崩溃的生产代码。对于一次性脚本或原型，部分严格规则可放宽。

---

## 一、编译配置：把检查开到最严

类型质量上限由 `tsconfig.json` 决定。下面是**生产项目应启用**的完整严格选项清单。

### 1.1 严格选项完整清单

| 选项                                 | 类型      | 推荐值   | 默认值       | 作用                                         |
| ---------------------------------- | ------- | ----- | --------- | ------------------------------------------ |
| strict                             | boolean | true  | false     | 一次性开启全部 strict 子选项，新增子选项会自动启用              |
| noImplicitAny                      | boolean | true  | 跟随 strict | 禁止隐式 any，未注解的参数报错                          |
| strictNullChecks                   | boolean | true  | 跟随 strict | null 与 undefined 不再可赋给任意类型                 |
| strictFunctionTypes                | boolean | true  | 跟随 strict | 函数参数双变性检查（更严格）                             |
| strictBindCallApply                | boolean | true  | 跟随 strict | bind/call/apply 的参数类型检查                    |
| strictPropertyInitialization       | boolean | true  | 跟随 strict | 类属性必须在构造器中初始化或带 ! 断言                       |
| noImplicitThis                     | boolean | true  | 跟随 strict | 禁止隐式 any 类型的 this                          |
| useUnknownInCatchVariables         | boolean | true  | 跟随 strict | catch (e) 中 e 类型为 unknown 而非 any           |
| alwaysStrict                       | boolean | true  | 跟随 strict | 输出文件强制 "use strict"                        |
| noUncheckedIndexedAccess           | boolean | true  | false     | 索引访问（arr\[0\]、obj\[key\]）返回 T \| undefined |
| exactOptionalPropertyTypes         | boolean | true  | false     | { x?: string } 不允许显式赋 undefined            |
| noImplicitReturns                  | boolean | true  | false     | 函数所有分支必须有 return                           |
| noFallthroughCasesInSwitch         | boolean | true  | false     | switch 的 case 必须 break/return              |
| noImplicitOverride                 | boolean | true  | false     | 子类覆盖父类方法必须加 override                       |
| noPropertyAccessFromIndexSignature | boolean | true  | false     | 索引签名属性必须用 obj\["key"\] 而非 obj.key          |
| allowUnreachableCode               | boolean | false | undefined | 不允许死代码                                     |
| allowUnusedLabels                  | boolean | false | undefined | 不允许未使用的标签                                  |

### 1.2 推荐的 tsconfig.json 基线

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,

    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,

    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules"]
}

```

### 1.3 实践要点

**实践 1 — 严格模式必开，不留退路。**

`"strict": true` 是底线。每次 TS 大版本都会向 `strict` 加入新子选项（如 5.0 的 `useUnknownInCatchVariables`），一次开启就能自动跟上。

```jsonc
// 正确
{ "strict": true }

// 错误：手动列出子选项，错过未来新增的检查
{ "noImplicitAny": true, "strictNullChecks": true }

```

**实践 2 — 开启 `noUncheckedIndexedAccess`，承认索引访问可能为空。**

数组与对象的索引访问默认返回 `T`，这是 TS 类型系统最大的不安全口子。开启后能强制你处理"键不存在"的情况。

```ts
const users: string[] = ["alice"];

// 关闭时：users[10] 推断为 string，运行时返回 undefined → 隐藏 bug
// 开启后：users[10] 推断为 string | undefined，必须先判空
const u = users[10];
if (u !== undefined) {
  console.log(u.toUpperCase());
}

```

**实践 3 — 开启 `exactOptionalPropertyTypes`，区分"缺失"与"显式 undefined"。**

```ts
interface Config {
  port?: number;
}

// exactOptionalPropertyTypes: false 时允许，但语义混乱
const c1: Config = { port: undefined };

// exactOptionalPropertyTypes: true 时报错；想"清除"必须用 delete 或不提供该 key
const c2: Config = {};

```

这能避免 `JSON.stringify` 序列化时 `undefined` 被丢失导致的客户端/服务端不一致。

---

## 二、类型设计：让类型表达意图

类型不是注释——它是**编译期可执行的规约**。好的类型让非法状态无法被构造。

### 2.1 优先类型推断，注解只在边界

TS 的局部推断非常强，过度注解反而让重构变难。

| 场景               | 是否注解        | 理由           |
| ---------------- | ----------- | ------------ |
| 局部变量 const x = 1 | 不注解         | 推断准确         |
| 函数返回值（公开 API）    | **必须注解**    | 防止内部修改意外改变签名 |
| 函数返回值（内部小函数）     | 可不注解        | 减少噪音         |
| 函数参数             | **必须注解**    | 参数无法推断       |
| 导出常量对象           | 用 satisfies | 既校验又保留窄类型    |

```ts
// 正确：边界注解，内部推断
export function fetchUser(id: string): Promise<User> {
  const url = `/api/users/${id}`;   // 不注解 string
  return http.get<User>(url);
}

// 错误：过度注解
export function fetchUser(id: string): Promise<User> {
  const url: string = `/api/users/${id}`;  // 噪音
  const path: string = "/api/users/";       // 噪音
  return http.get<User>(url);
}

```

### 2.2 `unknown` 而非 `any`

`any` 关闭类型检查并向外传染；`unknown` 是"未知但安全的"——必须先收窄才能使用。

```ts
// 错误：any 让整个调用链失去类型
function parse(json: string): any {
  return JSON.parse(json);
}
const data = parse('{"name":"a"}');
data.name.toUpperCase();  // 编译通过，运行时可能崩

// 正确：unknown 强制调用方先校验
function parse(json: string): unknown {
  return JSON.parse(json);
}
const data = parse('{"name":"a"}');
if (isUser(data)) {
  data.name.toUpperCase();  // 安全
}

```

唯一可以接受 `any` 的地方：与无类型 JS 互操作的临时桥，**必须配 `// TODO` 与 issue 链接**。

### 2.3 用判别联合（discriminated union）表达状态

把"互斥状态"建模为带共同字面量字段的联合，TS 能在分支内自动收窄。

```ts
// 错误：可选字段组合出非法状态
interface Resp {
  loading: boolean;
  data?: User;
  error?: Error;
}
// 非法但能构造：{ loading: true, data: user, error: err }

// 正确：判别联合
type Resp =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };

function render(r: Resp) {
  switch (r.status) {
    case "success":
      return r.data.name;       // 自动收窄，无需 r.data!
    case "error":
      return r.error.message;
    case "loading":
    case "idle":
      return "...";
  }
}

```

### 2.4 用 `as const` 与 `satisfies` 固化字面量

```ts
// 错误：类型被泛化为 string
const role = "admin";              // string
const config = { mode: "dev" };    // { mode: string }

// 正确 — as const：递归字面量化
const role = "admin" as const;     // "admin"
const config = { mode: "dev" } as const;  // { readonly mode: "dev" }

// 正确 — satisfies：校验形状但保留窄类型
type Theme = Record<string, { fg: string; bg: string }>;
const themes = {
  light: { fg: "#000", bg: "#fff" },
  dark:  { fg: "#fff", bg: "#000" },
} satisfies Theme;

themes.light.fg;     // string，类型安全
themes.unknown;      // 报错：使用 satisfies 后保留 key 字面量

```

`satisfies` 是 TS 4.9 引入、5.x 推广的特性，**优先于** `: Theme` 注解，因为后者会把 `themes.light` 拓宽为 `Theme[string]`。

### 2.5 品牌类型（Branded Types）区分语义相同的原始类型

`UserId` 与 `OrderId` 都是 `string`，但混用是 bug。用品牌防止误传。

```ts
type Brand<T, B> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function asUserId(s: string): UserId {
  return s as UserId;
}

function getUser(id: UserId) { /* ... */ }

const uid = asUserId("u_1");
const oid = "o_1" as OrderId;

getUser(uid);   // OK
getUser(oid);   // 报错：OrderId 不能赋给 UserId
getUser("u_1"); // 报错：string 不能赋给 UserId

```

### 2.6 用 `Readonly` 与 `ReadonlyArray` 表达不可变

```ts
// 函数声明它不会修改输入
function sum(nums: ReadonlyArray<number>): number {
  // nums.push(1);  // 报错
  return nums.reduce((a, b) => a + b, 0);
}

// 对象参数同理
function render(user: Readonly<User>) { /* ... */ }

```

不要在函数内部用 `as` 强行去掉 `readonly`——这是设计意图被破坏的信号。

### 2.7 `never` 用于穷举检查

```ts
type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; size: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.size ** 2;
    default:
      // 新增 kind 但忘了处理时，此处编译报错
      const _exhaustive: never = s;
      throw new Error(`unhandled: ${JSON.stringify(_exhaustive)}`);
  }
}

```

封装成工具函数：

```ts
function assertNever(x: never): never {
  throw new Error(`unexpected: ${JSON.stringify(x)}`);
}

```

---

## 三、函数与 API 设计

### 3.1 参数对象化（命名参数模式）

超过 2 个参数时，用对象传参代替位置参数，配合解构。

```ts
// 错误：调用方需要记住顺序与含义
function createUser(name: string, age: number, admin: boolean, email?: string) {}
createUser("alice", 30, true);  // true 是什么？

// 正确
interface CreateUserInput {
  name: string;
  age: number;
  admin: boolean;
  email?: string;
}
function createUser(input: CreateUserInput) {}
createUser({ name: "alice", age: 30, admin: true });

```

布尔参数尤其要避免直接传——`createUser("alice", 30, true, false, true)` 完全不可读。

### 3.2 公开 API 显式声明返回类型

```ts
// 错误：返回类型靠推断，重构时悄悄变化
export async function getUser(id: string) {
  return await db.query("SELECT * FROM users WHERE id=$1", [id]);
}
// 后续有人改了 query 返回类型，调用方全部受影响但不报错

// 正确
export async function getUser(id: string): Promise<User> {
  return await db.query("SELECT * FROM users WHERE id=$1", [id]);
}

```

内部辅助函数可以省略，但**任何被外部模块 import 的函数**都要显式声明。

### 3.3 重载用于"输入类型决定输出类型"的真实多态

```ts
// 正确：输入与输出严格对应
function parse(input: string): object;
function parse(input: Buffer): Buffer;
function parse(input: string | Buffer): object | Buffer {
  return typeof input === "string" ? JSON.parse(input) : input;
}

const a = parse("{}");           // object
const b = parse(Buffer.from("")); // Buffer

```

**反面：** 不要用重载模拟可选参数——直接用可选参数或联合类型。

### 3.4 优先返回 `Promise<T>`，避免回调

```ts
// 错误
function read(path: string, cb: (err: Error | null, data?: string) => void) {}

// 正确
async function read(path: string): Promise<string> {}

```

如果必须保留回调（如事件订阅），返回**清理函数**：

```ts
function subscribe(handler: (e: Event) => void): () => void {
  emitter.on("x", handler);
  return () => emitter.off("x", handler);
}

```

---

### 3.5 异步函数接受 `AbortSignal` 实现可取消

**问题：** 长时间运行的异步任务（HTTP、轮询、流读取、防抖的 fetch）若无法外部取消，会导致内存泄漏、组件卸载后仍触发副作用、用户切换路由时浪费网络请求。

**约定：** 任何可能耗时的异步函数都应在 options 对象中接受 `signal?: AbortSignal`，调用方负责创建与触发取消。

```ts
interface FetchOptions {
  signal?: AbortSignal;
  timeoutMs?: number;
}

async function fetchUser(id: string, opts: FetchOptions = {}): Promise<User> {
  // 组合外部取消信号与超时信号
  const signals: AbortSignal[] = [];
  if (opts.signal) signals.push(opts.signal);
  if (opts.timeoutMs) signals.push(AbortSignal.timeout(opts.timeoutMs));

  const r = await fetch(`/api/users/${id}`, {
    signal: signals.length ? AbortSignal.any(signals) : undefined,
  });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json();
}

// 调用方：组件卸载或路由切换时取消
const ctrl = new AbortController();
fetchUser("u1", { signal: ctrl.signal, timeoutMs: 5000 })
  .then(use)
  .catch(e => {
    if (e.name === "AbortError" || e.name === "TimeoutError") return;
    throw e;
  });

// React useEffect 清理函数
useEffect(() => {
  const ctrl = new AbortController();
  fetchUser(id, { signal: ctrl.signal }).then(setUser).catch(silenceAbort);
  return () => ctrl.abort();
}, [id]);

```

**AbortSignal 标准 API：**

| API                                  | 用途                   | 平台支持                   |
| ------------------------------------ | -------------------- | ---------------------- |
| new AbortController()                | 创建可手动触发的控制器          | 全支持                    |
| controller.abort(reason?)            | 触发取消，可附带原因           | 全支持                    |
| controller.signal                    | 拿到对应的 signal         | 全支持                    |
| AbortSignal.timeout(ms)              | 创建到时自动取消的信号          | Node 17.3+、Chrome 103+ |
| AbortSignal.any(\[s1, s2\])          | 组合多个信号，任一触发即取消       | Node 20+、Chrome 116+   |
| signal.aborted                       | 同步检查是否已取消（boolean）   | 全支持                    |
| signal.reason                        | 取消原因（abort 传入的参数）    | Node 17.2+、Chrome 100+ |
| signal.throwIfAborted()              | 已取消则抛出 signal.reason | Node 17.3+、Chrome 100+ |
| signal.addEventListener("abort", cb) | 监听取消事件               | 全支持                    |

**设计要点：**

- options 对象包裹 signal，便于未来扩展字段（`timeoutMs`、`retries` 等）不破坏签名
- 取消错误的 `name` 为 `"AbortError"`，超时的 `name` 为 `"TimeoutError"`，调用方按 `name` 分支
- 服务端框架（Express/Fastify/Hono）通过 `req.signal` 暴露请求级 signal——透传到下游 fetch 可实现"客户端断开则取消上游"
- Node `stream` / `db query` / `child_process.spawn` 等都已原生支持 `signal` 选项

**反模式：** 用 `setTimeout` \+ 手动标志位模拟取消——容易漏判，无法与 fetch、stream 等标准 API 组合。

---

## 四、泛型：约束优先于自由

### 4.1 总是用 `extends` 约束泛型

无约束的泛型几乎等于 `any`，无法对参数做任何操作。

```ts
// 错误：T 内部无法访问任何属性
function getId<T>(x: T) {
  return x.id;  // 报错
}

// 正确
function getId<T extends { id: string }>(x: T): string {
  return x.id;
}

```

### 4.2 泛型默认值减少调用方负担

```ts
interface ApiResp<T = unknown, E = Error> {
  data?: T;
  error?: E;
}

const r: ApiResp = await fetch("...");        // T = unknown
const u: ApiResp<User> = await fetch("...");  // T = User

```

### 4.3 不要为了泛型而泛型

如果调用方只会用一种类型，泛型就是噪音。

```ts
// 错误：永远只传 string，泛型毫无意义
function logKey<T extends string>(k: T): void {
  console.log(k);
}

// 正确
function logKey(k: string): void {
  console.log(k);
}

```

经验法则：**类型参数至少出现 2 次**（一次在输入，一次在输出或另一个输入）才值得引入。

### 4.4 用 `infer` 提取嵌套类型，但优先用内置工具类型

```ts
// 内置 ReturnType 已能解决大部分场景
function fetchUser() { return { id: "1", name: "a" }; }
type User = ReturnType<typeof fetchUser>;

// 自定义提取也用 infer
type Awaited<T> = T extends Promise<infer U> ? U : T;
type FirstArg<T> = T extends (a: infer A, ...rest: any[]) => any ? A : never;

```

常用的内置工具类型应该背下来：`Partial`、`Required`、`Readonly`、`Pick`、`Omit`、`Record`、`ReturnType`、`Parameters`、`Awaited`、`NonNullable`、`Extract`、`Exclude`、`InstanceType`。

---

### 4.5 高级模式：模板字面量类型与递归条件类型

`infer` \+ 递归条件类型 + 模板字面量类型让 TS 类型系统具备图灵完备能力。能写但要克制——过度使用会让编译器爆炸、IDE 卡顿。

#### 4.5.1 模板字面量类型实战

```ts
// 1) API 路径前缀强制
type ApiPath = `/api/${string}`;
declare function get(path: ApiPath): Promise<unknown>;
get("/api/users");   // OK
get("/users");       // 报错

// 2) CSS 单位约束
type CSSLength = `${number}px` | `${number}rem` | `${number}%` | "auto" | "0";

// 3) 事件名格式
type DOMEventName = `on${Capitalize<string>}`;

// 4) snake_case → camelCase 类型转换
type CamelCase<S extends string> =
  S extends `${infer P}_${infer R}`
    ? `${P}${Capitalize<CamelCase<R>>}`
    : S;

type T1 = CamelCase<"user_first_name">;   // "userFirstName"
type T2 = CamelCase<"already_camel">;     // "alreadyCamel"

// 5) 路由参数提取
type ExtractParams<S extends string> =
  S extends `${string}:${infer P}/${infer R}`
    ? P | ExtractParams<`/${R}`>
    : S extends `${string}:${infer P}`
      ? P
      : never;

type P = ExtractParams<"/users/:userId/posts/:postId">;  // "userId" | "postId"

```

#### 4.5.2 嵌套对象路径类型

实现类似 lodash `_.get(obj, "a.b.c")` 的类型安全访问：

```ts
type Paths<T, K extends keyof T = keyof T> =
  K extends string
    ? T[K] extends Record<string, unknown>
      ? `${K}` | `${K}.${Paths<T[K]>}`
      : `${K}`
    : never;

interface Config {
  server: { port: number; host: string };
  db: { user: string; pass: string };
}

type ConfigPath = Paths<Config>;
// "server" | "db" | "server.port" | "server.host" | "db.user" | "db.pass"

function get<T, P extends Paths<T>>(obj: T, path: P): unknown {
  return path.split(".").reduce((o: any, k) => o?.[k], obj);
}

get(config, "server.port");      // 类型安全，IDE 自动补全
get(config, "server.unknown");   // 报错

```

#### 4.5.3 DeepReadonly / DeepPartial

```ts
type DeepReadonly<T> =
  T extends (...args: any[]) => any
    ? T
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

type DeepPartial<T> =
  T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    : T;

type RoConfig = DeepReadonly<Config>;        // 所有嵌套属性都 readonly
type PartialConfig = DeepPartial<Config>;    // 所有嵌套属性都可选

```

#### 4.5.4 边界与代价

| 模式               | 推荐深度上限      | 风险                                               |
| ---------------- | ----------- | ------------------------------------------------ |
| 模板字面量 split      | ≤ 3 层嵌套     | 每层都是新的字符串实例化                                     |
| 递归条件类型           | ≤ 25 层      | 超过 50 层触发 Type instantiation is excessively deep |
| 联合 × 递归          | 联合 ≤ 8 项    | 联合大小 × 递归深度爆炸增长                                  |
| Paths<T>         | 对象字段总数 ≤ 30 | IDE 类型提示卡顿                                       |
| CamelCase 等字符串变换 | 字符串长度 ≤ 50  | 短字符串无碍，长字符串拖慢                                    |

**经验法则：** 写复杂类型前先问"这个能在运行时校验吗"——能就别在类型层写。**类型 ≠ 运行时**，让两层各司其职。

---

## 五、模块与类型导入

### 5.1 启用 `verbatimModuleSyntax`，明确区分类型与值

TS 5.0 引入的强力选项：**import/export 语句保持原样输出**，因此必须显式标注哪些是类型。

```ts
// 配合 verbatimModuleSyntax: true

// 正确
import type { User } from "./user";       // 仅类型，编译后被擦除
import { fetchUser } from "./user";       // 运行时值
import { type Role, hasRole } from "./auth";  // 混合时用 inline type

// 错误：用 import 引入纯类型，可能导致 CJS/ESM 互操作问题
import { User } from "./user";  // verbatimModuleSyntax 下报错

```

类型导入被擦除还能解决**循环依赖**问题——`import type` 不会在运行时建立依赖关系。

### 5.2 避免 `namespace`

`namespace` 是 TS 早期产物，与 ES 模块功能重叠。除非维护历史代码或写 `.d.ts` 环境声明，**永远用 ES 模块**。

```ts
// 错误
namespace Utils {
  export function fmt(d: Date) { /* ... */ }
}

// 正确
// utils.ts
export function fmt(d: Date) { /* ... */ }

```

### 5.3 谨慎使用桶文件（barrel files）

`index.ts` 重新导出整个目录看起来优雅，但有显著代价：

- 打包工具难以 tree-shake → 包体积膨胀
- 循环依赖更容易出现
- 大型仓库下编译变慢

**经验：** 库的公开入口可以用桶文件；项目内部业务模块直接相对路径导入即可。

### 5.4 路径别名替代 `../../..`

```jsonc
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

```

注意：**`tsc` 不会改写路径**——构建器（Vite、esbuild、tsup）或 `tsc-alias` 必须同步配置。

---

## 六、空值与防御性编程

### 6.1 可选链 `?.` vs 非空断言 `!`

| 用法        | 何时使用                     | 风险      |
| --------- | ------------------------ | ------- |
| obj?.prop | 不确定 obj 是否存在             | 无       |
| obj!.prop | **绝对**确定 obj 存在（且能解释为什么） | 错了会运行时崩 |

```ts
// 正确：DOM 查询结果必然可能为 null
const btn = document.querySelector("#submit");
btn?.addEventListener("click", handler);

// 错误：滥用 ! 把责任推给运行时
const btn = document.querySelector("#submit")!;
btn.addEventListener("click", handler);  // 元素不存在时崩溃

// 例外：构造器之后注入的依赖、ref 在 mounted 后必然有值
class Service {
  private db!: DB;  // 在 init() 中赋值
  async init() { this.db = await connect(); }
}

```

### 6.2 `??` 而非 `||`，避免 `0`/`""`/`false` 误判

```ts
// 错误：port = 0、name = "" 时会被覆盖
const port = config.port || 3000;
const name = user.name || "anonymous";

// 正确：仅 null/undefined 时取默认值
const port = config.port ?? 3000;
const name = user.name ?? "anonymous";

```

### 6.3 用类型守卫而不是断言

```ts
// 错误：断言关闭检查
function process(x: unknown) {
  const u = x as User;
  return u.name;  // x 不是 User 时崩
}

// 正确：类型守卫
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null && "id" in x && "name" in x;
}

function process(x: unknown) {
  if (isUser(x)) return x.name;
  throw new TypeError("not a user");
}

```

外部数据（HTTP 响应、`JSON.parse`、`localStorage`）**必须**经过 Zod / Valibot / ArkType 等运行时校验库。

---

## 七、错误处理

### 7.1 `catch` 变量是 `unknown`

启用 `useUnknownInCatchVariables` 后：

```ts
try {
  await op();
} catch (e) {
  // e 是 unknown，不能直接 e.message
  if (e instanceof Error) {
    log(e.message);
  } else {
    log(String(e));
  }
}

```

封装工具函数减少样板：

```ts
function toError(e: unknown): Error {
  return e instanceof Error ? e : new Error(String(e));
}

```

### 7.2 自定义错误用 `class` 而非字符串/枚举

```ts
class NotFoundError extends Error {
  constructor(public readonly resource: string, public readonly id: string) {
    super(`${resource} ${id} not found`);
    this.name = "NotFoundError";
  }
}

try {
  await getUser("u1");
} catch (e) {
  if (e instanceof NotFoundError) {
    // 类型收窄，e.resource / e.id 可用
  }
}

```

继承 `Error` 后必须显式设置 `this.name`，否则 `instanceof` 在跨 realm（iframe/worker）场景可能失效。

### 7.3 显式返回 `Result` 类型处理可预期错误

对于业务可预期的失败（用户不存在、权限不足），不抛异常，返回判别联合：

```ts
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function getUser(id: string): Promise<Result<User, NotFoundError>> {
  const u = await db.find(id);
  return u
    ? { ok: true, value: u }
    : { ok: false, error: new NotFoundError("user", id) };
}

const r = await getUser("u1");
if (r.ok) {
  use(r.value);
} else {
  handle(r.error);
}

```

异常仅留给"程序无法继续"的情况（OOM、配置错误、网络断开）。

---

### 7.4 用 Zod / Valibot 统一运行时校验与类型

**问题：** 外部数据（HTTP 响应、表单输入、`localStorage`、配置文件、URL 参数）的类型只是"承诺"。承诺被打破时，`as` 注解的代码运行时崩溃且追踪困难。

**解决：** 用运行时校验库定义 schema，从 schema 自动推断 TS 类型——**单点真相**，schema 改了类型同步变化。

```ts
import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(["admin", "user", "guest"]),
  createdAt: z.coerce.date(),   // 自动把 ISO 字符串转 Date
});

// 类型从 schema 推断，永不脱节
type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string): Promise<User> {
  const r = await fetch(`/api/users/${id}`);
  const data: unknown = await r.json();
  return UserSchema.parse(data);   // 失败抛 ZodError，含完整错误路径
}

// 安全模式 + Result 模式
function parseUser(data: unknown): Result<User, z.ZodError> {
  const r = UserSchema.safeParse(data);
  return r.success
    ? { ok: true, value: r.data }
    : { ok: false, error: r.error };
}

```

**Zod 常用 API：**

| API                              | 类型                                            | 说明                                      |
| -------------------------------- | --------------------------------------------- | --------------------------------------- |
| Schema.parse(data)               | (data: unknown) => T                          | 严格校验，失败抛 ZodError                       |
| Schema.safeParse(data)           | (data: unknown) => { success, data \| error } | 返回判别联合，不抛错                              |
| Schema.parseAsync(data)          | (data: unknown) => Promise<T>                 | 异步校验（含 async refine）                    |
| z.infer<typeof S>                | type util                                     | 推断输出类型                                  |
| z.input<typeof S>                | type util                                     | 推断输入类型（与 output 在 transform/coerce 后不同） |
| Schema.partial()                 | () => Schema                                  | 所有字段变可选                                 |
| Schema.required()                | () => Schema                                  | 所有字段变必选                                 |
| Schema.pick({ name: true })      | (mask) => Schema                              | 挑选字段                                    |
| Schema.omit({ pwd: true })       | (mask) => Schema                              | 排除字段                                    |
| Schema.extend({ x: z.string() }) | (shape) => Schema                             | 扩展字段                                    |
| Schema.refine(v => ok, "msg")    | (check, msg) => Schema                        | 自定义校验逻辑                                 |
| Schema.transform(v => f(v))      | (fn) => Schema                                | 校验后转换，可改输出类型                            |
| Schema.default(v)                | (value) => Schema                             | 缺失时填默认值                                 |
| Schema.catch(v)                  | (value) => Schema                             | 校验失败时填回退值                               |

**同一 schema 多场景复用：**

```ts
const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  password: z.string().min(8),
});

const CreateUserInput = UserSchema;                                       // 注册表单
const UpdateUserInput = UserSchema.partial().omit({ password: true });    // 更新表单
const PublicUser      = UserSchema.omit({ password: true });              // 对外响应

type CreateInput = z.infer<typeof CreateUserInput>;
type UpdateInput = z.infer<typeof UpdateUserInput>;
type PublicUser  = z.infer<typeof PublicUser>;

```

**库选择对比：**

| 库             | bundle (gzip) | 类型推断 | 风格                   | 适合场景                          |
| ------------- | ------------- | ---- | -------------------- | ----------------------------- |
| Zod           | \~14 KB       | 优秀   | 链式                   | 通用首选                          |
| Valibot       | \~1 KB        | 优秀   | 函数式 + tree-shakeable | bundle 敏感（移动 / Edge / Worker） |
| ArkType       | \~10 KB       | 优秀   | 字符串语法（类 TS 字面）       | 偏好简洁字面                        |
| Yup           | \~12 KB       | 一般   | 链式                   | 老项目 / Formik 生态               |
| io-ts         | \~12 KB       | 优秀   | 函数式                  | fp-ts 生态                      |
| Effect Schema | \~30 KB+      | 优秀   | 函数式                  | 已用 Effect 框架                  |

**反模式：**

- 手写守卫 + 重复定义 TS 类型——改一边忘了另一边
- 用 `as User` 绕过校验后被业务依赖——任何上游变更都是定时炸弹
- 在多处 parse 同一份数据——只在边界（HTTP handler、表单提交）做一次

---

## 八、与第三方库协作

### 8.1 优先选有官方类型的库

包目录或 `package.json` 含 `"types"` 字段说明库自带类型，否则需要 `@types/*`。

| 来源                         | 标识                                 | 注意            |
| -------------------------- | ---------------------------------- | ------------- |
| 包内自带                       | package.json 有 "types" 或 "typings" | 最新最准          |
| @types/\*（DefinitelyTyped） | npm i -D @types/<pkg>              | 社区维护，可能滞后     |
| 无类型                        | 上述都没有                              | 自己写 .d.ts 或换库 |

### 8.2 扩展第三方类型用 `declare module`

```ts
// types/express.d.ts —— 给 Express Request 添加自定义字段
import "express";

declare module "express" {
  interface Request {
    userId?: string;
  }
}

```

确保该 `.d.ts` 文件被 tsconfig 的 `include` 覆盖。

### 8.3 用 `import type` 引入仅做类型用的依赖

避免运行时被打包：

```ts
import type { Plugin } from "vite";

export default function myPlugin(): Plugin { /* ... */ }

```

构建产物中不会包含对 `vite` 的 `require`/`import`。

---

### 8.4 编写 `.d.ts` 文件的三种模式

第三方库没类型、或需要给现有库扩展类型时，会用到三种不同的 `.d.ts` 写法。**选错模式类型不会被识别**——这是新手最常踩的坑。

#### 模式 A：模块声明（给整个 npm 包加类型）

适用：`some-lib` 既没自带类型，也没有 `@types/some-lib`，需要自己写。

```ts
// src/types/some-lib.d.ts
declare module "some-lib" {
  export interface Options {
    timeout?: number;
    retries?: number;
  }
  export function doSomething(opts?: Options): Promise<string>;
  const _default: (input: string) => void;
  export default _default;
}

```

**关键点：**

- 文件名任意，扩展名必须是 `.d.ts`
- 确保 `tsconfig.include` 覆盖到 `src/types/**/*`
- 顶层不要有 `import`/`export`——一旦成为模块，`declare module` 的作用域会变

#### 模式 B：模块扩充（给已有类型的包加字段/方法）

适用：Express Request 加 `userId`、Vite 配置加自定义字段、给 Vue 全局组件加类型。

```ts
// src/types/express.d.ts
import "express";   // 必须先 import，激活模块扩充语义

declare module "express" {
  interface Request {
    userId?: string;
    requestId: string;
  }
}

```

**关键点：**

- **必须**有 `import` 或 `export` 让该文件成为模块。否则 `declare module "express"` 会被当成**重新声明**整个模块，把原有类型全部覆盖（导致 `app.use` 都报错）
- 只能扩充 `interface`，不能扩充 `type`（前者支持声明合并）
- 扩充的属性建议加 `?` 可选，避免破坏未赋值场景

#### 模式 C：环境声明（全局变量、特殊文件类型）

适用：Vite `import.meta.env`、`*.svg` 资源导入、注入到 `window` 的全局对象、Webpack 等定义的常量。

```ts
// src/types/global.d.ts
declare global {
  interface Window {
    __APP_VERSION__: string;
    __INITIAL_STATE__: unknown;
  }

  const __DEV__: boolean;
  const __BUILD_TIME__: string;
}

// 资源模块导入
declare module "*.svg" {
  const content: string;
  export default content;
}

declare module "*.svg?component" {
  import type { Component } from "vue";
  const c: Component;
  export default c;
}

declare module "*.module.css" {
  const classes: Record<string, string>;
  export default classes;
}

export {};  // 让文件成为模块，激活 declare global

```

**关键点：**

- `declare global` **必须**写在模块内——文件末加 `export {}` 把它变成模块
- 全局变量用 `declare const`、全局类型用 `interface` 或 `type`
- Vite 项目通常用 `<reference types="vite/client" />` 一键拉入 `import.meta.env` 类型，自己写时遵循 Vite 文档

#### 三种模式速查表

| 想做什么             | 文件顶部需要           | 用什么语法                                 | 典型文件名               |
| ---------------- | ---------------- | ------------------------------------- | ------------------- |
| 给无类型的包加类型        | 无需 import/export | declare module "包名"                   | types/lib-name.d.ts |
| 给已有类型的包扩字段       | import "包名" 激活   | declare module "包名" { interface ... } | types/express.d.ts  |
| 加全局变量/类型         | 末尾 export {}     | declare global { ... }                | types/global.d.ts   |
| 给资源类型加 import 支持 | 无需 import/export | declare module "\*.ext"               | types/assets.d.ts   |

**校验工作正常的最快方式：** 在使用处写 `import { x } from "lib"`，看 IDE 是否补全。不补全说明 `.d.ts` 未被识别（检查 `include` / 路径 / 文件名）。

---

## 九、性能与构建

### 9.1 启用 `skipLibCheck`

绝大多数项目都应该开。`@types/*` 之间的类型冲突不是你的责任，关掉能省 30%+ 的编译时间。

```jsonc
{ "skipLibCheck": true }

```

例外：你正在开发库，需要确保发布的类型与依赖兼容。

### 9.2 启用 `isolatedModules`

要求每个文件能被单文件转译器（esbuild、swc、Babel）独立处理。这是使用 Vite / Next.js / tsup 的前提。

```jsonc
{ "isolatedModules": true }

```

启用后会禁止：`const enum`、跨文件 type-only 重新导出（除非用 `export type`）等只有 `tsc` 全量编译才能正确处理的语法。

### 9.3 用 Project References 拆分巨型仓库

单仓库代码超过 \~50k LOC 时，把项目拆成多个 `tsconfig.json` 并用 `references` 串起来：

```jsonc
// tsconfig.json
{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/cli" }
  ]
}

```

每个子项目独立增量编译，大幅提速 IDE 与 CI。

### 9.4 类型计算别失控

避免在热路径写出几百行的递归条件类型——TS 会触发 instantiation depth limit (50)，且 IDE 卡顿。

```ts
// 危险：超长字符串模板字面量解析
type ParseURL<S extends string> = /* 30 行递归 */;

// 更好：保留类型为 string，把校验放到运行时
function parseURL(s: string): URL { /* ... */ }

```

---

### 9.5 类型层面的性能优化

大型项目 IDE 卡顿、`tsc` 慢的根因往往是**类型计算过重**而非文件数量。源码 1 万行的项目如果有几个失控的递归条件类型，能让编译时间从 5 秒涨到 5 分钟。

#### 诊断手段

```bash
# 输出扩展诊断信息（耗时分布）
tsc --extendedDiagnostics --noEmit

# 生成类型计算追踪
tsc --generateTrace ./trace --noEmit
# 把生成的 trace/ 目录拖进 chrome://tracing 或 https://ui.perfetto.dev

```

关键指标：

| 指标             | 健康值       | 异常含义              |
| -------------- | --------- | ----------------- |
| Instantiations | < 200k    | 类型实例化过多，递归 / 联合爆炸 |
| Types          | < 100k    | 类型总量过大，考虑拆模块      |
| Memory used    | < 1 GB    | 内存压力，IDE 易卡顿      |
| Total time     | < 30s（中型） | 超过则诊断             |

#### 5 条优化原则

**1) 优先 `interface` 而非长链 `type & type`**

`interface` 有内部缓存，大型交叉类型每次实例化都重新计算。

```ts
// 慢
type Big = A & B & C & D & E;

// 快
interface Big extends A, B, C, D, E {}

```

**2) 大型联合类型提到顶层**

```ts
// 慢：每次使用 Lang 都重新计算
function tr(s: string, lang: "zh" | "en" | "fr" | "de" | "ja" | "ko" | "ru" | "es"): string {}

// 快
type Lang = "zh" | "en" | "fr" | "de" | "ja" | "ko" | "ru" | "es";
function tr(s: string, lang: Lang): string {}

```

**3) 缓存复杂工具类型的结果**

```ts
// 慢：每次使用 ExtractKeys<User> 都要计算
function f(): ExtractKeys<User> {}
function g(): ExtractKeys<User> {}

// 快：命名一次，复用
type UserKey = ExtractKeys<User>;
function f(): UserKey {}
function g(): UserKey {}

```

**4) 限制递归条件类型深度**

```ts
// 危险：无深度限制的递归
type Paths<T> = /* ... */;

// 改进：加深度计数器
type Paths<T, D extends number = 5, Acc extends number[] = []> =
  Acc["length"] extends D ? never : /* 递归用 [...Acc, 0] */;

```

**5) 必开 `skipLibCheck: true`**

绕过 `node_modules/**/*.d.ts` 的类型检查，省 30%+ 编译时间。仅库作者发版时可关闭。

#### 当一个文件特别慢

```bash
# 找出最慢的文件
tsc --diagnostics --noEmit | grep "instantiations"
# 或看 trace 输出中 instantiations 最高的 source file

```

常见原因：

- 大型 `Record<string, ComplexType>` 字面量（路由表、i18n 词典）
- 深嵌套泛型组合（`A<B<C<D<E>>>>`）
- 长字符串模板字面量类型解析（SQL/正则 AST）

**修复策略：** 把字面量类型断言为更宽的类型，把"精确推断"换成"运行时校验"。

---

## 十、代码组织与命名

### 10.1 类型命名规范

| 类型      | 命名                 | 示例                        |
| ------- | ------------------ | ------------------------- |
| 接口/类型别名 | PascalCase         | User、ApiResponse          |
| 泛型参数    | 单字母大写或 T 前缀        | T、K、TKey、TResult          |
| 枚举与枚举成员 | PascalCase         | LogLevel.Info             |
| 字面量联合   | 小写字符串              | "idle" \| "loading"       |
| 布尔字段    | 形容词或 is/has/can 前缀 | enabled、isAdmin、hasAccess |

**不要**给 interface 加 `I` 前缀（`IUser`）——这是 C# 习惯，TS 社区已抛弃。

### 10.2 `interface` vs `type` 的选择

| 选 interface 当  | 选 type 当          |
| -------------- | ----------------- |
| 描述对象/类的形状      | 联合、交叉、元组、原始类型     |
| 需要被 implements | 需要工具类型如 Pick<...> |
| 期望第三方扩展（声明合并）  | 需要条件类型            |

**默认用 `interface`**，遇到上述 `type` 场景才切换。两者性能差异在 TS 5.x 已基本消除。

### 10.3 避免巨型类型文件

把类型与使用它的实现放在同一文件，除非：

- 类型被 ≥ 3 处使用 → 抽到 `types.ts`
- 公共领域模型 → `src/types/<domain>.ts`

**不要**建一个全局 `src/types/index.ts` 把全项目类型都塞进去——会成为修改瓶颈。

---

### 10.4 ESLint + typescript-eslint 推荐规则

类型系统不能捕获所有问题：未 await 的 Promise、误用 Promise 作为 boolean、循环中的副作用、未使用的变量。typescript-eslint 是补充。

#### 最小推荐配置（typescript-eslint v8+，Flat Config）

```js
// eslint.config.js
import tseslint from "typescript-eslint";

export default tseslint.config(
  tseslint.configs.strictTypeChecked,
  tseslint.configs.stylisticTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // 强烈推荐打开
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/await-thenable": "error",
      "@typescript-eslint/require-await": "error",
      "@typescript-eslint/no-unnecessary-condition": "warn",
      "@typescript-eslint/switch-exhaustiveness-check": "error",
      "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
      "@typescript-eslint/no-import-type-side-effects": "error",
      "@typescript-eslint/no-explicit-any": "warn",
      "@typescript-eslint/no-non-null-assertion": "warn",
      "@typescript-eslint/no-unused-vars": ["error", {
        argsIgnorePattern: "^_",
        varsIgnorePattern: "^_",
      }],
      "@typescript-eslint/prefer-nullish-coalescing": "error",
      "@typescript-eslint/prefer-optional-chain": "error",

      // 按项目偏好调整
      "@typescript-eslint/explicit-module-boundary-types": "off",  // 库项目改为 error
      "@typescript-eslint/no-empty-object-type": "warn",
    },
  },
);

```

#### 必开规则的解释

| 规则                          | 防止的 bug                                  |
| --------------------------- | ---------------------------------------- |
| no-floating-promises        | 漏 await → unhandled rejection → 进程崩溃     |
| no-misused-promises         | if (asyncFn()) 永为 true（Promise 是 truthy） |
| await-thenable              | 对非 Promise 用 await → 静默 bug              |
| require-await               | async 函数体内无 await → 多余的 Promise 包装       |
| no-unnecessary-condition    | x !== undefined 而 x 类型已确定非空              |
| switch-exhaustiveness-check | 联合类型新增 case 时忘了处理                        |
| consistent-type-imports     | 配合 verbatimModuleSyntax 自动加 type 修饰符     |
| no-import-type-side-effects | 防止 type-only import 触发副作用                |
| prefer-nullish-coalescing   | 该用 ?? 时用 \||                             |
| prefer-optional-chain       | 该用 ?. 时用 && 链                            |

#### 库项目额外开启

```js
rules: {
  "@typescript-eslint/explicit-module-boundary-types": "error",
  "@typescript-eslint/explicit-function-return-type": ["error", {
    allowExpressions: true,
    allowTypedFunctionExpressions: true,
  }],
  "@typescript-eslint/no-explicit-any": "error",
}

```

#### 配套：Prettier + Editorconfig

ESLint 管"语义问题"，Prettier 管"格式"。两者分工：

```json
// .prettierrc.json
{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "printWidth": 100
}

```

不在 ESLint 中开启格式规则（如 `semi`、`indent`）——交给 Prettier。

#### CI 集成

```json
// package.json
{
  "scripts": {
    "lint": "eslint . --max-warnings 0",
    "typecheck": "tsc --noEmit",
    "ci": "npm run typecheck && npm run lint && npm test"
  }
}

```

`--max-warnings 0` 让 warning 也阻止 CI——逼团队真正处理而不是积累。

---

## 十一、语法选择决策：多用什么、少用什么、何时用什么

TypeScript 的语法表面相似但语义差距巨大。本节用四个角度回答"实战中怎么选"：

1. **场景 → 语法对照表**：按你想做什么反查最佳语法
2. **多用清单**：推荐高频使用的语法及其解决的问题
3. **少用/避免清单**：解释为什么避开、用什么替代
4. **易混淆语法对比**：相似语法之间的精确边界

---

### 11.1 场景 → 语法对照表

按"我想做什么"反查。**先看这张表，再去查细节。**

| 你想做什么                              | 推荐语法                                        | 避免语法                                  | 一句话理由                 |            |
| ---------------------------------- | ------------------------------------------- | ------------------------------------- | --------------------- | ---------- |
| 固化字面量类型                            | as const                                    | : "literal" 注解                        | as const 递归冻结整个对象     |            |
| 校验类型又保留窄推断                         | satisfies                                   | : T 注解                                | 注解会拓宽类型               |            |
| 表达互斥状态（idle/loading/success/error） | 判别联合 + kind 字段                              | 多个可选 boolean 标志                       | 判别联合让非法状态无法构造         |            |
| 区分同型不同义（UserId vs OrderId）         | 品牌类型 T & { \_\_brand }                      | 类型别名 type UserId = string             | 别名可互换，品牌不可            |            |
| 表达不可变数组                            | ReadonlyArray<T> 或 readonly T\[\]           | T\[\] \+ 约定                           | 类型层面阻止 push           |            |
| 表达不可变对象                            | Readonly<T> 或 as const                      | 注释"请勿修改"                              | 编译期保障                 |            |
| 收窄 unknown/联合类型                    | 类型守卫 x is T \+ typeof/instanceof/in         | as T 断言                               | 守卫是检查，断言是 bypass      |            |
| 公开 API 输入参数 ≥ 3 个                  | 命名参数对象 fn({ a, b, c })                      | 位置参数 fn(a, b, c)                      | 调用方不需要记顺序             |            |
| 表达"可能空但要先校验"                       | unknown \+ 守卫                               | any                                   | unknown 强制处理          |            |
| 表达"绝对不会发生"                         | never                                       | throw 而不声明                            | never 让穷举检查生效         |            |
| 表达"调用此函数永不返回"                      | : never 返回类型                                | : void                                | void 可继续执行后续代码        |            |
| 表达"调用方不应使用返回值"                     | : void 返回类型                                 | : undefined                           | 接口语义不同                |            |
| 跨模块仅引入类型                           | import type { X } from "..."                | import { X } from "..."               | 类型擦除避免循环依赖            |            |
| 在同一 import 混引类型和值                  | import { type X, fn } from "..."            | 拆成两条 import                           | 减少噪音                  |            |
| 提取函数返回类型                           | ReturnType<typeof fn>                       | 手写副本类型                                | 单点真相                  |            |
| 提取函数参数类型                           | Parameters<typeof fn>                       | 手写副本类型                                | 同上                    |            |
| 提取 Promise 内部类型                    | Awaited<T>                                  | T extends Promise<infer U> ? U : T 手写 | 已内置                   |            |
| 从对象类型挑字段                           | Pick<T, "a" \| "b">                         | 重新定义类型                                | 跟随源类型演化               |            |
| 从对象类型去字段                           | Omit<T, "secret">                           | 重新定义类型                                | 同上                    |            |
| 创建键值对查找表                           | Record<K, V>                                | { \[k: string\]: V }                  | 更易读                   |            |
| 表达"可能为 null/undefined"             | T \| undefined 或 T                          | null                                  | T? 在类型位置              | ? 仅在属性位置合法 |
| 给可选字段去掉 undefined                  | NonNullable<T>                              | T & {} 黑魔法                            | 内置工具更明确               |            |
| 把可选字段变必选                           | Required<T>                                 | 重新定义                                  | 同上                    |            |
| 把必选字段变可选                           | Partial<T>                                  | 重新定义                                  | 同上                    |            |
| 链式访问可能空的属性                         | a?.b?.c 可选链                                 | a && a.b && a.b.c                     | 简洁且与 ?? 配合            |            |
| 提供"仅 null/undefined"默认值            | x ?? default                                | x \|| default                         | \|| 会覆盖 0/""/false    |            |
| 类的真正私有字段                           | #field（ES 私有字段）                             | private field（TS 关键字）                 | # 在运行时也私有             |            |
| 表达"可能是任何形状的对象"                     | Record<string, unknown>                     | object 或 {}                           | 后两者语义不明确              |            |
| 表达"任何非 nullish 值"                  | {} 或 NonNullable<unknown>                   | Object                                | Object 会接受 null 表现混乱  |            |
| 表达"任何函数"                           | (...args: never\[\]) => unknown             | Function                              | Function 是黑洞          |            |
| 表达"任何数组"                           | readonly unknown\[\]                        | any\[\]                               | 后者关闭检查                |            |
| 给第三方库扩展类型                          | declare module "lib" { ... }                | 复制声明文件                                | 维护友好                  |            |
| 给全局对象加属性                           | declare global { interface Window { ... } } | 改 lib.d.ts                            | 项目内自治                 |            |
| 工具类型变形                             | type X = Pick<...> & Omit<...>              | 写新 interface                          | 自动跟随源                 |            |
| 写 React/Vue 组件 Props               | interface Props {}                          | type Props = {}                       | 期望被合并/扩展              |            |
| 写联合/交叉/元组                          | type X = A \| B                             | interface                             | interface 不支持联合       |            |
| 给参数加默认值                            | 解构默认 function f({ x = 1 } = {})             | function f(x?) { x = x ?? 1 }         | 调用方与签名一致              |            |
| 表达字符串枚举                            | as const 对象 + keyof                         | enum E { A = "a" }                    | 无运行时副作用               |            |
| 表达数字枚举                             | as const 对象 + keyof                         | enum E { A, B }                       | 同上                    |            |
| 调用方要 implements 实现                 | interface                                   | type                                  | type 不推荐用于 implements |            |
| 反射式获取对象 keys                       | Object.keys(o) as Array<keyof typeof o>     | 直接 Object.keys(o)                     | TS 故意返回 string\[\]    |            |
| 表达"接受任何对象但只读其结构"                   | Readonly<Record<string, unknown>>           | any                                   | 不丢类型检查                |            |

---

### 11.2 应该多用的语法（按解决问题分组）

#### A. 类型推断与精确化

**1) `as const` —— 解决"字面量被自动拓宽"**

**问题：** `const role = "admin"` 推断为 `string`，丢失字面量信息。

**何时用：** 定义配置对象、字面量集合、希望被联合类型消费的字符串。

```ts
// 错误
const roles = ["admin", "user", "guest"];        // string[]
type Role = (typeof roles)[number];               // string

// 正确
const roles = ["admin", "user", "guest"] as const;  // readonly ["admin", "user", "guest"]
type Role = (typeof roles)[number];                  // "admin" | "user" | "guest"

```

**2) `satisfies` —— 解决"既要校验形状，又要保留窄类型"**

**问题：** `: T` 注解会把变量类型拓宽为 `T`，丢失原始字面量；`as T` 则会跳过检查。

**何时用：** 定义符合某接口的对象常量、路由表、配置树。

```ts
type RouteConfig = Record<string, { path: string; auth: boolean }>;

// 错误 — 注解：通过校验但类型被拓宽，丢失 key 自动补全
const routes: RouteConfig = {
  home: { path: "/", auth: false },
  admin: { path: "/admin", auth: true },
};
routes.unknown;  // 不报错，因为类型是 Record<string, ...>

// 错误 — 断言：跳过检查
const routes = { /* 形状错误也不报 */ } as RouteConfig;

// 正确 — satisfies：校验 + 保留窄类型
const routes = {
  home: { path: "/", auth: false },
  admin: { path: "/admin", auth: true },
} satisfies RouteConfig;
routes.unknown;  // 报错：只有 "home" | "admin"

```

**3) 判别联合（discriminated union）—— 解决"用可选字段建模状态"**

**问题：** 多个 `optional` 字段组合可以构造出非法状态（如同时 loading 和 error）。

**何时用：** 异步状态、表单状态、有限状态机、消息协议。

```ts
type Msg =
  | { type: "text"; content: string }
  | { type: "image"; url: string; alt?: string }
  | { type: "system"; code: number };

function render(m: Msg) {
  switch (m.type) {
    case "text":   return m.content;       // 自动收窄
    case "image":  return m.url;
    case "system": return `[${m.code}]`;
  }
}

```

**判别字段命名约定：** `type` / `kind` / `tag` / `status` 任选其一，团队内一致即可。

**4) 类型守卫 `x is T` —— 解决"如何安全把 unknown 收窄"**

**问题：** `as T` 关闭检查；`typeof`/`instanceof` 表达能力有限。

**何时用：** 校验外部数据、运行时多态分支、`Array.prototype.filter` 等高阶用法。

```ts
function isString(x: unknown): x is string {
  return typeof x === "string";
}

const arr: (string | null)[] = ["a", null, "b"];
const clean: string[] = arr.filter(isString);  // 类型正确

// 复合守卫
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null
    && "id" in x && typeof (x as any).id === "string"
    && "name" in x && typeof (x as any).name === "string";
}

```

**生产建议：** 复杂数据用 Zod / Valibot / ArkType 自动生成守卫，不要手写。

**5) `never` 穷举检查 —— 解决"新增 case 忘了处理"**

**何时用：** 任何 `switch` 处理判别联合的场景。

```ts
function assertNever(x: never): never {
  throw new Error(`unhandled: ${JSON.stringify(x)}`);
}

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.size ** 2;
    default: return assertNever(s);  // 新增 kind 时此处编译报错
  }
}

```

#### B. 不可变与防御

**6) `readonly` / `ReadonlyArray` —— 解决"不希望被修改的参数被修改"**

**何时用：** 任何函数参数（除非显式需要 mutate）、共享配置、Redux/Zustand state。

```ts
function sum(nums: readonly number[]): number {
  // nums.push(1);  // 报错
  return nums.reduce((a, b) => a + b, 0);
}

```

**7) `?.` 可选链 —— 解决"链式访问中间环节可能为 null"**

```ts
// 错误：层层 &&
const street = user && user.address && user.address.street;

// 正确
const street = user?.address?.street;

```

**调用方法时也适用：** `obj.method?.()`、`arr?.[0]`。

**8) `??` 空值合并 —— 解决"`||` 误把 0/false/'' 当无效值"**

```ts
// 错误
const port = config.port || 3000;        // port=0 时变 3000
const enabled = config.enabled || true;  // enabled=false 时变 true

// 正确
const port = config.port ?? 3000;
const enabled = config.enabled ?? true;

```

**配合赋值：** `x ??= defaultValue` 仅在 `x` 为 null/undefined 时赋值。

#### C. 类型变形与提取

**9) `Pick` / `Omit` / `Partial` / `Required` / `Readonly` —— 解决"派生类型重复定义"**

**何时用：** 从领域模型派生 DTO、表单类型、更新输入。

```ts
interface User {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

type PublicUser = Omit<User, "password">;                 // 对外暴露
type UpdateUser = Partial<Pick<User, "name" | "email">>;  // 更新接口
type NewUser    = Omit<User, "id" | "createdAt">;         // 创建接口
type UserView   = Readonly<PublicUser>;                   // 视图层

```

**10) `Record<K, V>` —— 解决"查找表/字典类型表达"**

```ts
type StatusColor = Record<"idle" | "loading" | "error", string>;

const colors: StatusColor = {
  idle: "gray",
  loading: "blue",
  error: "red",
};
// 漏一个 key 会报错

```

**11) `ReturnType` / `Parameters` / `Awaited` —— 解决"从函数反推类型"**

```ts
async function fetchUser(id: string) {
  return { id, name: "alice" };
}

type FetchUserArgs = Parameters<typeof fetchUser>;            // [id: string]
type FetchUserRet  = ReturnType<typeof fetchUser>;            // Promise<{id, name}>
type User          = Awaited<ReturnType<typeof fetchUser>>;   // {id, name}

```

**12) `NonNullable<T>` —— 解决"去掉可空"**

```ts
type Maybe = string | null | undefined;
type Sure = NonNullable<Maybe>;  // string

```

#### D. 模块与依赖

**13) `import type` 与 inline `type` —— 解决"仅类型用途的运行时副作用"**

```ts
// 仅类型
import type { User } from "./models";

// 混合：把 type 修饰符放在导入项前
import { type Config, loadConfig } from "./config";

```

**何时必须用：** 启用了 `verbatimModuleSyntax` 时（推荐配置）。

**14) 命名参数对象 —— 解决"参数顺序难记 / 布尔参数语义不明"**

```ts
// 错误
function createWindow(width: number, height: number, modal: boolean, closable: boolean) {}
createWindow(800, 600, true, false);  // 后两个 boolean 是什么？

// 正确
interface WindowOptions {
  width: number;
  height: number;
  modal?: boolean;
  closable?: boolean;
}
function createWindow(opts: WindowOptions) {}
createWindow({ width: 800, height: 600, modal: true });

```

**阈值：** ≥ 3 个参数、或含 ≥ 1 个布尔/枚举参数时强制使用。

#### E. 高级（按需使用）

**15) 品牌类型 —— 解决"同型不同义的混用"**  
**16) 模板字面量类型 —— 解决"字符串字面量需要约束格式"**

```ts
type HexColor = `#${string}`;
type EventName = `on${Capitalize<string>}`;

const ok: HexColor = "#ff0000";
const bad: HexColor = "ff0000";  // 报错

```

**何时用：** API 路径校验、CSS 单位、事件名。**不要**为字符串解析写超过 30 行的递归模板类型。

**17) `infer` 条件类型 —— 解决"提取嵌套结构中的某个类型"**

```ts
type Unwrap<T> = T extends Array<infer U> ? U : T;
type FirstArg<F> = F extends (a: infer A, ...rest: any[]) => any ? A : never;

```

**优先级：** 先查内置工具类型 → 再用 `infer` 自己写。

---

### 11.3 应该少用或避免的语法（按危险等级排序）

#### 🔴 高危：几乎永远不要用

**1) `any` —— 类型黑洞**

**为什么避开：** 关闭类型检查，且向外传染（`any.foo.bar` 不报错）。

**替代：** `unknown`（安全的未知）→ 用类型守卫收窄。

**唯一例外：** 与无类型 JS 库桥接，且必须配 `// FIXME: 待加类型` \+ issue。

**2) `as unknown as T` —— 双重断言强制转换**

**为什么避开：** 完全绕过类型系统，等同 `any` 但更隐蔽。

**替代：** 用类型守卫；如果是数据校验，用 Zod。

```ts
// 错误
const user = JSON.parse(s) as unknown as User;

// 正确
const data: unknown = JSON.parse(s);
const user = UserSchema.parse(data);

```

**3) `!` 非空断言（无证据时）**

**为什么避开：** 把"我以为这里非空"承诺给编译器，错了就运行时崩。

**替代：** `?.` \+ `??`、提前判空抛错、`assertExists(x)` 工具。

```ts
// 错误：DOM 查询不能保证元素存在
const el = document.getElementById("x")!;

// 正确
const el = document.getElementById("x");
if (!el) throw new Error("missing #x");
el.click();

```

**允许例外：** 类属性 `db!: DB` 配合 `init()` 注入；Vue/React `ref` 在 `mounted` 后的访问。这两种情况要写注释解释。

**4) `@ts-ignore` —— 无差别屏蔽错误**

**为什么避开：** 屏蔽**所有**错误，包括未来新出现的；不强制写理由。

**替代：** `@ts-expect-error <理由>`。后者在错误消失后会自己报错，提醒你删掉。

```ts
// 错误
// @ts-ignore
const x: number = "abc";

// 正确
// @ts-expect-error 第三方类型定义错误，已提 PR #123
const x: number = "abc";

```

#### 🟡 中危：特定场景可以用，但默认避开

**5) `enum`（特别是数字 enum）—— 运行时副作用 + 双向映射陷阱**

**为什么避开：**

- 普通 `enum` 编译出真实对象（增加包体积）
- 数字 enum 是双向映射 `E[0] === "A"`，反向查到字符串令人困惑
- 与 `isolatedModules` 不能完全配合（`const enum` 受限）

**替代：** `as const` 对象 + `keyof typeof`。

```ts
// 避免
enum Status { Idle, Loading, Success }

// 推荐
const Status = { Idle: "idle", Loading: "loading", Success: "success" } as const;
type Status = (typeof Status)[keyof typeof Status];  // "idle" | "loading" | "success"

```

**例外：** 大量遗留代码已用 `enum`，新加成员保持一致；与 Protobuf/数据库列对应的整数枚举。

**6) `namespace` / `module` 关键字 —— 与 ES 模块功能重叠**

**为什么避开：** TS 早期产物，与 ES 模块功能重叠；现代打包工具无法 tree-shake namespace 内部。

**替代：** ES 模块文件 + `import` / `export`。

**例外：** 编写 `.d.ts` 环境声明、扩展全局对象（`declare global { namespace ... }`）。

**7) 函数重载（伪多态时）**

**为什么避开：** 仅当输入类型决定输出类型时才需要重载；模拟可选参数或联合时用更简单的语法。

```ts
// 错误：用重载模拟"可选参数"
function greet(name: string): string;
function greet(): string;
function greet(name?: string): string {
  return `hi ${name ?? "stranger"}`;
}

// 正确
function greet(name?: string): string {
  return `hi ${name ?? "stranger"}`;
}

```

**何时该用重载：** 输入类型与输出类型严格绑定（`parse(string): object; parse(Buffer): Buffer`）。

**8) 类的 `private` 关键字 —— 仅编译期私有**

**为什么避开：** 运行时仍可被 `obj["secret"]` 或反序列化访问；JS 已有真正的私有字段 `#field`。

**替代：** `#field`（ES 私有字段）。

```ts
// 编译期私有，运行时可绕过
class A { private secret = 1; }
const a = new A();
(a as any).secret;  // 1

// 运行时私有
class B { #secret = 1; }
const b = new B();
(b as any)["#secret"];  // undefined

```

**例外：** Vue 3 reactivity 与 `#field` 不兼容时（实例属性需被代理）；某些 ORM 框架的元编程依赖反射。

**9) 方法语法 `name(): void` 用于回调字段 —— 双变性陷阱**

**为什么避开：** 方法语法在 strict 模式下仍是双变（bivariant），会接受不安全的子签名。

**替代：** 属性语法 `name: () => void`。

```ts
// 错误：方法语法
type Handler = { onClick(e: MouseEvent): void };
const h: Handler = { onClick: (e: Event) => {} };  // 通过，但 e.button 会丢失

// 正确：属性语法
type Handler = { onClick: (e: MouseEvent) => void };
const h: Handler = { onClick: (e: Event) => {} };  // 同样通过（逆变）
const h2: Handler = { onClick: (e: KeyboardEvent) => {} };  // 报错（按预期）

```

**经验：** 对象的"方法"用方法语法，对象的"回调字段"用属性语法。

**10) `Function` / `Object` / `{}` 顶层类型**

**为什么避开：**

- `Function` 没有调用签名，且接受任何对象
- `Object` 接受 `null`/`undefined` 之外的几乎所有值（包括基本类型）
- `{}` 等价于"非 null/undefined 的任何值"，常被误解为"空对象"

**替代：**

| 想表达        | 用                                                           |
| ---------- | ----------------------------------------------------------- |
| 任意可调用      | (...args: never\[\]) => unknown 或 (...args: any\[\]) => any |
| 任意对象（字面意义） | Record<string, unknown> 或 object                            |
| 任意值        | unknown                                                     |
| 空对象        | Record<string, never>                                       |

#### 🟢 低危：风格选择，团队内一致即可

**11) `Array<T>` vs `T[]` —— 选一种风格**

简单元素用 `T[]`，复合类型用 `Array<T>` 提升可读性：

```ts
const ids: string[] = [];
const handlers: Array<(e: Event) => void> = [];  // 比 `((e: Event) => void)[]` 易读

```

**12) `interface` vs `type` —— 见 §10.2 决策表**

**13) 三斜杠引用 `/// <reference />` —— 现代项目避开**

**替代：** `import type` 或 `tsconfig.compilerOptions.types`。

**14) 旧式装饰器（`experimentalDecorators`）—— 等待标准**

**为什么避开：** TS 5.0 已支持 Stage 3 装饰器（不带 `experimentalDecorators`）；旧实现与新标准不兼容。

**例外：** Angular、NestJS、TypeORM 等框架强制要求旧装饰器；切换需等框架支持。

**15) 桶文件 `export * from "./*"` —— 大型项目谨慎**

**为什么避开：** 阻碍 tree-shaking、加剧循环依赖、拖慢编译。

**何时用：** 库的对外入口（一个 `index.ts`）；项目内业务模块直接相对路径导入。

---

### 11.4 易混淆语法精确对比

#### `as` vs `satisfies` vs `: T` 注解

| 语法                      | 做什么                                  | 何时用             |
| ----------------------- | ------------------------------------ | --------------- |
| const x: T = v          | 校验 v 符合 T，并把 x 的类型设为 T（**拓宽**）       | 函数参数、返回值显式声明    |
| const x = v as T        | 强制把 v 视为 T，**不校验**（仅在子类型/父类型关系内宽松允许） | 与第三方数据交互、收窄已知类型 |
| const x = v satisfies T | 校验 v 符合 T，但 x 的类型**保留** v 的窄推断       | 字面量常量、配置对象、路由表  |

```ts
type Theme = Record<string, string>;
const a: Theme = { dark: "#000" };                 // a 类型 = Theme，丢字面量
const b = { dark: "#000" } as Theme;               // 同上，且校验更弱
const c = { dark: "#000" } satisfies Theme;        // c 类型 = { dark: string }，保留 key
const d = { dark: "#000" as const } satisfies Theme; // d.dark 类型 = "#000"

```

#### `unknown` vs `any` vs `never`

| 类型      | 含义         | 可赋给它    | 它可赋给            |
| ------- | ---------- | ------- | --------------- |
| unknown | 安全的"任意值"   | 任何值     | 仅 unknown 和 any |
| any     | 关闭检查的"任意值" | 任何值     | 任何类型（危险）        |
| never   | 不可能存在的值    | 仅 never | 任何类型            |

**使用法则：**

- 入参想"接受任何东西"→ `unknown`
- 函数永不返回（抛错/无限循环）→ 返回 `never`
- 穷举检查、过滤联合 → `never`
- 你以为需要 `any` → 90% 应该是 `unknown`

#### `?? ` vs `||`

| 表达式     | 取右值的条件                                   |
| ------- | ---------------------------------------- |
| x \|| y | x 为 falsy（0/""/false/null/undefined/NaN） |
| x ?? y  | x 为 null 或 undefined                     |

```ts
0 || 10        // 10  ❌ 通常不希望
0 ?? 10        // 0   ✅
"" ?? "def"    // ""  ✅
null ?? "def"  // "def"

```

#### `?.` vs `&&` vs `!`

| 语法       | 行为                                 | 何时用        |
| -------- | ---------------------------------- | ---------- |
| a?.b     | a 为 null/undefined 时短路返回 undefined | 不确定 a 是否存在 |
| a && a.b | a 为 falsy 时返回 a（不是 undefined）      | 几乎已被 ?. 取代 |
| a!.b     | 告诉编译器 a 必非空，运行时不检查                 | 有充分证据时（少用） |

#### `readonly` vs `Readonly<T>` vs `as const`

| 语法                                | 深度     | 适用对象    |
| --------------------------------- | ------ | ------- |
| readonly T\[\] / ReadonlyArray<T> | 顶层     | 数组类型    |
| readonly 修饰符                      | 单个属性   | 接口/类型字段 |
| Readonly<T>                       | 顶层（一层） | 任意对象类型  |
| as const                          | 递归冻结   | 字面量值    |

```ts
interface Point { x: number; y: number; }

type R1 = Readonly<Point>;              // { readonly x: number; readonly y: number }
type R2 = readonly Point[];              // 数组只读，但元素可改
const p = { x: 1, y: 2 } as const;       // { readonly x: 1; readonly y: 2 } — 字面量化

```

#### `import type` vs 内联 `type` 修饰符

```ts
// 整条仅类型
import type { User, Role } from "./auth";

// 同一条语句混合
import { type Config, loadConfig } from "./config";

// 等价于
import type { Config } from "./config";
import { loadConfig } from "./config";

```

**何时整条 vs 内联：** 整条更明确；混引时用内联减少重复。

#### `interface` vs `type` 决策表

| 需求                         | 选 interface | 选 type |
| -------------------------- | ----------- | ------ |
| 描述对象/类形状                   | ✅           | ✅      |
| extends 继承                 | ✅           | 用 & 交叉 |
| implements                 | ✅           | ⚠️     |
| 联合 A \| B                  | ❌           | ✅      |
| 交叉 A & B                   | 用 extends   | ✅      |
| 元组 \[a, b\]                | ❌           | ✅      |
| 映射类型 { \[K in keyof T\] }  | ❌           | ✅      |
| 条件类型                       | ❌           | ✅      |
| 声明合并（同名自动合并）               | ✅           | ❌      |
| 第三方扩展（如扩展 Express Request） | ✅           | ❌      |

**默认规则：** 描述"对象/类"用 `interface`；其他全部场景用 `type`。

#### `void` vs `undefined` vs `never`（返回类型）

| 返回类型        | 含义                             |
| ----------- | ------------------------------ |
| : void      | 调用方不应使用返回值；实际可返回任意值（会被忽略）      |
| : undefined | 必须显式 return undefined 或 return |
| : never     | 函数永不正常返回（抛错或死循环）               |

```ts
function log(): void { console.log("x"); }                  // OK
function noop(): undefined { return; }                       // OK
function fail(msg: string): never { throw new Error(msg); }  // OK

// 回调中的 void 允许返回值被忽略：
[1, 2].forEach((x): void => x);  // 即使返回了 x 也合法

```

#### `Record<string, T>` vs 索引签名 `{ [k: string]: T }`

功能等价，但：

- `Record` 更短、更易读
- 索引签名可与具体属性同存：`{ name: string; [k: string]: unknown }`
- `Record` 配合联合 key：`Record<"a" | "b", T>` 是完全封闭的类型

---

### 11.5 经验法则汇总

1. **想固化字面量** → `as const`
2. **想校验形状又保留窄类型** → `satisfies`
3. **建模状态** → 判别联合 + 穷举检查
4. **收窄类型** → 类型守卫 `x is T`，不用 `as`
5. **传 3+ 参数或带布尔** → 命名参数对象
6. **代替 enum** → `as const` 对象
7. **代替 namespace** → ES 模块
8. **代替 `||` 取默认** → `??`
9. **代替手写嵌套判空** → `?.`
10. **代替 any** → `unknown` \+ 守卫
11. **代替 `private`** → `#field`
12. **代替 `Function`/`Object`** → 精确签名 / `Record<string, unknown>`
13. **代替手写派生类型** → `Pick`/`Omit`/`Partial`/`Required`
14. **代替手写函数签名提取** → `Parameters`/`ReturnType`/`Awaited`
15. **代替 `@ts-ignore`** → `@ts-expect-error <理由>`

---

## 十二、类型层面测试

类型本身也是代码——工具类型、复杂泛型、对外 API 的类型签名都需要测试。否则一次"无害的重构"会悄悄破坏下游用户的类型推断，而 CI 不报错。

### 12.1 用 `expectTypeOf`（Vitest 内置）

Vitest 1.x+ 自带 `expectTypeOf`，无需额外依赖，与运行时断言混写。

```ts
import { expectTypeOf, test } from "vitest";

type FetchUser = (id: string) => Promise<{ id: string; name: string }>;

test("fetchUser 类型契约", () => {
  expectTypeOf<FetchUser>().toBeFunction();
  expectTypeOf<FetchUser>().parameters.toEqualTypeOf<[string]>();
  expectTypeOf<FetchUser>().returns.resolves.toMatchTypeOf<{ id: string }>();
});

test("Pick 工具类型行为", () => {
  type User = { id: string; name: string; secret: string };
  type Public = Pick<User, "id" | "name">;
  expectTypeOf<Public>().toEqualTypeOf<{ id: string; name: string }>();
  expectTypeOf<Public>().not.toHaveProperty("secret");
});

test("可选字段处理", () => {
  type T = { a: string; b?: number };
  expectTypeOf<Required<T>>().toEqualTypeOf<{ a: string; b: number }>();
  expectTypeOf<Partial<T>>().toEqualTypeOf<{ a?: string; b?: number }>();
});

```

**核心 API：**

| API                                            | 检查                 |
| ---------------------------------------------- | ------------------ |
| .toEqualTypeOf<T>()                            | 类型完全相等（双向赋值）       |
| .toMatchTypeOf<T>()                            | 可赋给 T（结构子集即可）      |
| .toBeString() / .toBeNumber() / .toBeBoolean() | 原始类型               |
| .toBeNever() / .toBeUnknown() / .toBeAny()     | 特殊类型               |
| .toBeFunction() / .toBeObject()                | 顶层种类               |
| .toBeNullable()                                | 含 null 或 undefined |
| .parameters                                    | 函数参数（链式）           |
| .returns                                       | 函数返回值（链式）          |
| .resolves                                      | 解开 Promise（链式）     |
| .items                                         | 数组元素类型（链式）         |
| .toHaveProperty("k")                           | 对象有该字段             |
| .not                                           | 取反                 |

### 12.2 用 `Equal` / `Expect` 工具（无运行时依赖）

只想做类型相等检查、不依赖测试框架时，写两个工具类型：

```ts
// 来自 type-fest / 社区惯例
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type Expect<T extends true> = T;

// 把类型断言写成 type alias，编译失败就报错
type _t1 = Expect<Equal<Pick<{ a: 1; b: 2 }, "a">, { a: 1 }>>;
type _t2 = Expect<Equal<Awaited<Promise<number>>, number>>;
type _t3 = Expect<Equal<NonNullable<string | null>, string>>;

// 错误用例必须失败
type _bad = Expect<Equal<string, number>>;  // 报错：true 不能赋给 false

```

**`Equal` vs `extends` 的区别：** `Equal` 检查双向可赋值，能区分 `any` 和 `unknown`、`string` 和 `string | undefined`、`{ a: 1 }` 和 `{ a: 1; b?: undefined }`。`extends` 只查单向。

### 12.3 用 `tsd` / `dtslint`（库发布前）

发布 npm 包时，要确保**用户那一侧**的类型推断符合预期。`tsd` 是社区标准做法。

```ts
// test-d/my-lib.test-d.ts
import { expectType, expectError, expectAssignable, expectNotAssignable } from "tsd";
import { fetchUser, type User } from "../src";

// 返回类型必须严格相等
expectType<Promise<User>>(fetchUser("u1"));
expectType<string>((await fetchUser("u1")).name);

// 错误用法必须报编译错误
expectError(fetchUser(123));         // 参数类型错
expectError(fetchUser());            // 缺参数
expectError(fetchUser("u1", "extra")); // 多参数

// 子类型可赋值
expectAssignable<{ id: string }>({ id: "1", name: "a" } as User);
expectNotAssignable<{ secret: string }>({ id: "1", name: "a" } as User);

```

`package.json` 配置：

```json
{
  "scripts": {
    "test:types": "tsd"
  },
  "tsd": {
    "directory": "test-d"
  }
}

```

CI 跑 `npm run test:types`——任何用户感知的类型回归都会被捕获。

### 12.4 类型测试的写作原则

1. **测公开 API 的类型契约**，不测内部辅助类型——内部类型可以自由演化
2. **测边界**：可选字段、联合类型、`never`、`any`、`undefined`、空对象
3. **新增工具类型必须配类型测试**——否则一次"小改"破坏所有用户
4. **重构前先跑 `tsc --noEmit`**，再跑类型测试，再跑运行时测试
5. **类型测试与运行时测试同一文件**，方便看到契约 + 行为的双重保障

### 12.5 何时不需要类型测试

| 场景            | 是否需要   | 理由               |
| ------------- | ------ | ---------------- |
| 库 / SDK 对外发布  | **必要** | 用户类型推断是契约        |
| 内部工具类型库（团队复用） | 推荐     | 一次重构影响所有调用       |
| 应用项目内部类型      | 不需要    | tsc --noEmit 已足够 |
| 一次性脚本         | 不需要    | 投入产出不划算          |
| 类型只有一处使用      | 不需要    | 调用方即测试           |

**结论：** 库作者必做；应用作者按需做（核心工具类型值得测）。

---

## 十三、常见陷阱

### 陷阱 1：对象字面量的"多余属性检查"

**现象：** 把变量赋给类型时通过，直接传字面量时报错。

**原因：** 直接传字面量时 TS 会做额外的"多余属性检查"防拼写错误；中转变量后就放宽了。

**解决：** 修正拼写或显式接受额外字段。

```ts
interface Opts { url: string; method?: string; }

function req(o: Opts) {}

req({ url: "/a", methood: "GET" });  // 报错：methood 拼错

// 中转后不报错（TS 假设你知道自己在做什么），但仍是 bug
const o = { url: "/a", methood: "GET" };
req(o);  // 不报错 → 隐藏问题

// 正确做法：修拼写。如果真要接受额外字段，扩展接口
interface Opts { url: string; method?: string; [k: string]: unknown; }

```

### 陷阱 2：`as` 类型断言不做转换

**现象：** `const n = "abc" as unknown as number; n.toFixed(2)` 编译通过，运行时崩。

**原因：** `as` 只在编译期改变 TS 对类型的理解，不会生成任何转换代码。

**解决：** 用真正的转换函数（`Number()`、`String()`、`JSON.parse`），仅当你能向编译器证明类型时才用 `as`。

```ts
// 错误
const n = "abc" as unknown as number;

// 正确
const n = Number("abc");
if (Number.isNaN(n)) throw new Error("invalid number");

```

### 陷阱 3：函数参数双变性导致回调签名误判

**现象：** 数组方法、事件回调中传入的函数参数比预期"宽"。

**原因：** 函数类型默认是双变（bivariant）的；启用 `strictFunctionTypes` 后变成逆变（contravariant），但**方法语法**仍是双变。

```ts
type Handler = { onEvent(e: MouseEvent): void };  // 方法语法 → 双变
const h: Handler = { onEvent(e: Event) {} };       // 通过，但运行时可能崩

type Handler2 = { onEvent: (e: MouseEvent) => void };  // 属性语法 → 严格
const h2: Handler2 = { onEvent: (e: Event) => {} };    // 通过
const h3: Handler2 = { onEvent: (e: KeyboardEvent) => {} };  // 报错

```

**解决：** 写回调类型时用**属性语法** `name: (...) => T` 而非**方法语法** `name(...): T`。

### 陷阱 4：`enum` 在生产代码中产生运行时副作用

**现象：** 期望 `enum` 像类型一样擦除，结果打包后多出一段 IIFE。

**原因：** 普通 `enum` 在编译后会输出真实对象；`const enum` 虽然会被内联但**与 `isolatedModules` 冲突**。

**解决：** 用 `as const` 对象或字面量联合替代：

```ts
// 错误（现代项目）
enum Status { Idle, Loading, Success }

// 正确
const Status = { Idle: 0, Loading: 1, Success: 2 } as const;
type Status = (typeof Status)[keyof typeof Status];

```

### 陷阱 5：`Promise` 链中遗漏 `await` 导致 unhandled rejection

**现象：** 函数返回成功但错误没传播，进程异常退出。

**原因：** 缺少 `await`，错误进入微任务队列后无人处理。

**解决：** 启用 ESLint 规则 `@typescript-eslint/no-floating-promises`，强制 `await` 或显式 `.catch()` / `void`。

```ts
// 错误
async function main() {
  cleanup();          // 返回 Promise 但未 await
  await mainLogic();
}

// 正确
async function main() {
  await cleanup();
  await mainLogic();
}

// 故意 fire-and-forget 时显式标注
void cleanup();

```

### 陷阱 6：可选字段在条件类型中的 `undefined` 渗漏

**现象：** `T["x"]` 推断出意料之外的 `undefined`。

**原因：** `interface { x?: number }` 等价于 `x: number | undefined`，索引访问会带上 `undefined`。

**解决：** 用 `NonNullable<T["x"]>` 或 `Required<T>["x"]`：

```ts
interface User { name?: string; }

type N1 = User["name"];                  // string | undefined
type N2 = NonNullable<User["name"]>;     // string
type N3 = Required<User>["name"];        // string

```

### 陷阱 7：JSON.parse 返回 `any` 污染类型

**现象：** `const data = JSON.parse(s)` 返回 `any`，后续所有访问都不被检查。

**原因：** TS 内置签名就是 `any`。

**解决：** 包装成返回 `unknown` 的工具，配合校验：

```ts
function parseJSON(s: string): unknown {
  return JSON.parse(s);
}

const data = parseJSON(text);
const user = UserSchema.parse(data);  // Zod / Valibot 等

```

### 陷阱 8：`Object.keys` 返回 `string[]` 而非 `keyof T`

**现象：** 对对象迭代时拿不到具体 key 字面量类型。

**原因：** TS 选择不返回 `(keyof T)[]`，因为对象可能在运行时有额外的 key（结构子类型）。

**解决：** 已知对象封闭时显式断言：

```ts
const config = { host: "localhost", port: 5432 } as const;

// 错误：k 是 string，config[k] 报错
for (const k of Object.keys(config)) {
  console.log(config[k]);
}

// 正确
for (const k of Object.keys(config) as Array<keyof typeof config>) {
  console.log(config[k]);
}

// 或者用 entries
for (const [k, v] of Object.entries(config)) {
  console.log(k, v);
}

```

---

## 参见

- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/) — TS 类型系统、泛型、装饰器、tsconfig、Vue 项目最佳实践
- [JS模块系统](https://blog.vercanti.com/js-mjs-ts-wen-jian-qu-bie-yu-zui-jia-shi-jian/) — ESM/CJS 互操作与模块解析
- [JavaScript Promise 完全指南](https://blog.vercanti.com/javascript-promise-wan-quan-zhi-nan/) — Promise/async 与错误传播
- [JavaScript入门](https://blog.vercanti.com/javascript-ru-men/) — JS 语言基础（TS 之前先掌握）
- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/) — Vue 3 + TS 集成
- [React完全指南](https://blog.vercanti.com/react-wan-quan-zhi-nan/) — React + TS 模式
- [Vite初级指南](https://blog.vercanti.com/vite-chu-ji-zhi-nan/) — TS 项目的主流构建工具