TypeScript 最佳实践

相关文档: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

分享

官方文档: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完全指南 | JS模块系统 | JavaScript Promise 完全指南


目录

  1. 概述
  2. 编译配置:把检查开到最严
  3. 类型设计:让类型表达意图
  4. 函数与 API 设计
  5. 泛型:约束优先于自由
  6. 模块与类型导入
  7. 空值与防御性编程
  8. 错误处理
  9. 与第三方库协作
  10. 性能与构建
  11. 代码组织与命名
  12. 语法选择决策:多用什么、少用什么、何时用什么
  13. 类型层面测试
  14. 常见陷阱
  15. 参见

概述

What:本文不重复 TypeScript 语法手册(参见 TypeScript完全指南),而是回答"在真实项目里,怎样的 TS 代码才算高质量"——围绕类型设计、API 设计、错误处理、性能、与生态协作整理 35 条可执行实践。

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

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


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

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

1.1 严格选项完整清单

选项 类型 推荐值 默认值 作用
strict boolean true false 一次性开启全部 strict 子选项,新增子选项会自动启用
noImplicitAny boolean true 跟随 strict 禁止隐式 any,未注解的参数报错
strictNullChecks boolean true 跟随 strict nullundefined 不再可赋给任意类型
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 基线

{
  "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),一次开启就能自动跟上。

// 正确
{ "strict": true }

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

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

数组与对象的索引访问默认返回 T,这是 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"。

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 既校验又保留窄类型
// 正确:边界注解,内部推断
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 是"未知但安全的"——必须先收窄才能使用。

// 错误: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 能在分支内自动收窄。

// 错误:可选字段组合出非法状态
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 constsatisfies 固化字面量

// 错误:类型被泛化为 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)区分语义相同的原始类型

UserIdOrderId 都是 string,但混用是 bug。用品牌防止误传。

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 用 ReadonlyReadonlyArray 表达不可变

// 函数声明它不会修改输入
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 用于穷举检查

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)}`);
  }
}

封装成工具函数:

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

三、函数与 API 设计

3.1 参数对象化(命名参数模式)

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

// 错误:调用方需要记住顺序与含义
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 显式声明返回类型

// 错误:返回类型靠推断,重构时悄悄变化
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 重载用于"输入类型决定输出类型"的真实多态

// 正确:输入与输出严格对应
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>,避免回调

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

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

如果必须保留回调(如事件订阅),返回清理函数

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

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

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

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

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,便于未来扩展字段(timeoutMsretries 等)不破坏签名
  • 取消错误的 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,无法对参数做任何操作。

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

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

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

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 不要为了泛型而泛型

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

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

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

经验法则:类型参数至少出现 2 次(一次在输入,一次在输出或另一个输入)才值得引入。

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

// 内置 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;

常用的内置工具类型应该背下来:PartialRequiredReadonlyPickOmitRecordReturnTypeParametersAwaitedNonNullableExtractExcludeInstanceType


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

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

4.5.1 模板字面量类型实战

// 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") 的类型安全访问:

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

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 语句保持原样输出,因此必须显式标注哪些是类型。

// 配合 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 模块

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

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

5.3 谨慎使用桶文件(barrel files)

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

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

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

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

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

注意:tsc 不会改写路径——构建器(Vite、esbuild、tsup)或 tsc-alias 必须同步配置。


六、空值与防御性编程

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

用法 何时使用 风险
obj?.prop 不确定 obj 是否存在
obj!.prop 绝对确定 obj 存在(且能解释为什么) 错了会运行时崩
// 正确: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 误判

// 错误: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 用类型守卫而不是断言

// 错误:断言关闭检查
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.parselocalStorage必须经过 Zod / Valibot / ArkType 等运行时校验库。


七、错误处理

7.1 catch 变量是 unknown

启用 useUnknownInCatchVariables 后:

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

封装工具函数减少样板:

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

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

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 类型处理可预期错误

对于业务可预期的失败(用户不存在、权限不足),不抛异常,返回判别联合:

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 改了类型同步变化。

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 多场景复用:

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

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

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

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

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

避免运行时被打包:

import type { Plugin } from "vite";

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

构建产物中不会包含对 viterequire/import


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

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

模式 A:模块声明(给整个 npm 包加类型)

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

// 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 全局组件加类型。

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

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

关键点:

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

模式 C:环境声明(全局变量、特殊文件类型)

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

// 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、全局类型用 interfacetype
  • 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%+ 的编译时间。

{ "skipLibCheck": true }

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

9.2 启用 isolatedModules

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

{ "isolatedModules": true }

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

9.3 用 Project References 拆分巨型仓库

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

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

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

9.4 类型计算别失控

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

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

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

9.5 类型层面的性能优化

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

诊断手段

# 输出扩展诊断信息(耗时分布)
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 有内部缓存,大型交叉类型每次实例化都重新计算。

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

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

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

// 慢:每次使用 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) 缓存复杂工具类型的结果

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

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

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

// 危险:无深度限制的递归
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%+ 编译时间。仅库作者发版时可关闭。

当一个文件特别慢

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

常见原因:

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

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


十、代码组织与命名

10.1 类型命名规范

类型 命名 示例
接口/类型别名 PascalCase UserApiResponse
泛型参数 单字母大写或 T 前缀 TKTKeyTResult
枚举与枚举成员 PascalCase LogLevel.Info
字面量联合 小写字符串 "idle" | "loading"
布尔字段 形容词或 is/has/can 前缀 enabledisAdminhasAccess

不要给 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)

// 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 !== undefinedx 类型已确定非空
switch-exhaustiveness-check 联合类型新增 case 时忘了处理
consistent-type-imports 配合 verbatimModuleSyntax 自动加 type 修饰符
no-import-type-side-effects 防止 type-only import 触发副作用
prefer-nullish-coalescing 该用 ?? 时用 ||
prefer-optional-chain 该用 ?. 时用 &&

库项目额外开启

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 管"格式"。两者分工:

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

不在 ESLint 中开启格式规则(如 semiindent)——交给 Prettier。

CI 集成

// 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 | undefinedT | 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,丢失字面量信息。

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

// 错误
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 则会跳过检查。

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

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)。

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

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 等高阶用法。

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 处理判别联合的场景。

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。

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

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

// 错误:层层 &&
const street = user && user.address && user.address.street;

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

调用方法时也适用: obj.method?.()arr?.[0]

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

// 错误
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、表单类型、更新输入。

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> —— 解决"查找表/字典类型表达"

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

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

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

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> —— 解决"去掉可空"

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

D. 模块与依赖

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

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

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

何时必须用: 启用了 verbatimModuleSyntax 时(推荐配置)。

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

// 错误
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) 模板字面量类型 —— 解决"字符串字面量需要约束格式"

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

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

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

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

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。

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

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

3) ! 非空断言(无证据时)

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

替代: ?. + ??、提前判空抛错、assertExists(x) 工具。

// 错误: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 refmounted 后的访问。这两种情况要写注释解释。

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

为什么避开: 屏蔽所有错误,包括未来新出现的;不强制写理由。

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

// 错误
// @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

// 避免
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) 函数重载(伪多态时)

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

// 错误:用重载模拟"可选参数"
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 私有字段)。

// 编译期私有,运行时可绕过
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

// 错误:方法语法
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> 提升可读性:

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

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

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

替代: import typetsconfig.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 的窄推断 字面量常量、配置对象、路由表
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 安全的"任意值" 任何值 unknownany
any 关闭检查的"任意值" 任何值 任何类型(危险)
never 不可能存在的值 never 任何类型

使用法则:

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

?? vs ||

表达式 取右值的条件
x || y x 为 falsy(0/""/false/null/undefined/NaN
x ?? y xnullundefined
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 递归冻结 字面量值
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 修饰符

// 整条仅类型
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 undefinedreturn
: never 函数永不正常返回(抛错或死循环)
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. 代替 enumas const 对象
  7. 代替 namespace → ES 模块
  8. 代替 || 取默认??
  9. 代替手写嵌套判空?.
  10. 代替 anyunknown + 守卫
  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,无需额外依赖,与运行时断言混写。

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() nullundefined
.parameters 函数参数(链式)
.returns 函数返回值(链式)
.resolves 解开 Promise(链式)
.items 数组元素类型(链式)
.toHaveProperty("k") 对象有该字段
.not 取反

12.2 用 Equal / Expect 工具(无运行时依赖)

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

// 来自 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 检查双向可赋值,能区分 anyunknownstringstring | undefined{ a: 1 }{ a: 1; b?: undefined }extends 只查单向。

12.3 用 tsd / dtslint(库发布前)

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

// 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 配置:

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

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

12.4 类型测试的写作原则

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

12.5 何时不需要类型测试

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

结论: 库作者必做;应用作者按需做(核心工具类型值得测)。


十三、常见陷阱

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

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

原因: 直接传字面量时 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

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

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

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

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

原因: 函数类型默认是双变(bivariant)的;启用 strictFunctionTypes 后变成逆变(contravariant),但方法语法仍是双变。

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 对象或字面量联合替代:

// 错误(现代项目)
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

// 错误
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"]

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 的工具,配合校验:

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(结构子类型)。

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

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);
}

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog