← Blog
·10 blog.minutes

现代 TypeScript:2026 年的模式与实践

从严格配置到类型安全的 API 客户端——在 2026 年,这些 TypeScript 模式将生产级代码与玩具项目区分开来。

TypeScript 的发展速度超过了大多数生态系统能够跟上的速度。每个版本都会带来新的语法、更严格的检查和重写惯用模式的模式。仅仅是能编译的 TypeScript 代码与真正利用类型系统的代码之间的差距是巨大的——而这个差距决定了你的类型是文档还是噪音。

本文涵盖了 2026 年生产级 TypeScript 最重要的模式。这些不是学术练习。它们是使大型代码库更安全、API 更难被误用以及重构不那么可怕的做法。每个示例都来自处理数百万请求的生产系统中的真实模式。

基础:2026 年的 TypeScript 配置

你对 TypeScript 质量做出的最有影响力的决定不在你的代码中——它在你的 tsconfig.json 中。基线已经超越了 strict: true。在 2026 年,生产级配置必须启用以前是可选或实验性的检查。

// tsconfig.json — the 2026 baseline
{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

每个标志都消除了一类错误。exactOptionalPropertyTypes 防止了 ?: string | undefined 被显式赋值为 undefined 的常见错误——缺失和存在但未定义之间的区别在 API 边界上很重要。noUncheckedIndexedAccess 强制你为每个带有动态键的对象访问处理 undefined,这能在运行时崩溃被部署之前捕获它们。verbatimModuleSyntax 确保你的模块解析与运行时匹配,消除了破坏生产环境中 ESM 项目的静默不匹配。

采用这种配置的团队报告与空引用和未定义属性相关的生产事故显著减少。提前处理那些额外的 undefined 检查的开销与调试因为 API 响应缺少一个你假设存在的字段而导致的崩溃相比微不足道。

可辨识联合——你最强大的模式

可辨识联合是 TypeScript 中最有影响力的单一模式。它们显式地建模状态,使非法状态不可表示,并赋予编译器执行穷举处理所需的信息。如果你不为任何有多重形状的数据使用它们,你就是在对抗类型系统而不是让它为你工作。

type ApiState<S, E = Error> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: S }
  | { status: "error"; error: E };

// Exhaustive match — if you add a status, this breaks at compile time
function renderState<S>(state: ApiState<S>): string {
  switch (state.status) {
    case "idle":
      return "Awaiting input";
    case "loading":
      return "Loading...";
    case "success":
      return `Got ${JSON.stringify(state.data)}`;
    case "error":
      return `Failed: ${state.error.message}`;
  }
}

// Usage — impossible to access data on a loading state
const userState: ApiState<User> = { status: "loading" };
// userState.data — does not compile
// userState.status — narrows correctly after any check

神奇之处在于收窄。当你检查 state.status === "success" 时,TypeScript 确切知道你在哪个变体中,并为该分支上的每个字段提供正确的类型。这消除了运行时代码原本需要的整类防御性检查。编译器变成了你无效状态转换的测试套件。

为了让这个模式有效工作,判别属性(上面例子中的 status)必须是字面量类型,而不是一般的 string。每个变体必须有一个唯一的字面量值。编译器使用该字面量来区分分支,如果两个变体有相同的判别值它会警告你。

  • 始终使用字面量字符串或数字作为判别——永远不要用泛型字符串类型。
  • 保持共享属性最少化。变体之间不同的所有内容都放在变体对象内。
  • 与 never 类型结合进行穷举性检查:在 switch 的默认分支中声明一个 never 类型的变量,以在编译时捕获未处理的情况。

模板字面量类型和品牌类型

模板字面量类型和品牌类型解决两个不同的问题,普通的 TypeScript 类型无法处理。模板字面量类型在类型层面提供了字符串验证。品牌类型在结构类型系统中提供了名义类型——它们让你区分两个具有相同形状但不同语义含义的值。

// Template literal type — valid API routes are checked at compile time
type ApiRoute = `/api/${string}`;
type UserRoute = `/api/users/${string}`;

function fetchApi<T>(route: ApiRoute): Promise<T> {
  return fetch(route).then((r) => r.json());
}

// fetchApi("/invalid");       // Error: not assignable
// fetchApi("/api/users/123"); // OK

// Branded type — distinguish IDs that are both strings
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId): Promise<User> {
  return db.users.find(id);
}

const orderId = "ord_123" as OrderId;
// getUser(orderId); // Error: Type 'OrderId' is not assignable to type 'UserId'

模板字面量类型在任何字符串遵循可预测格式的系统中都大放异彩。API 路由构建器、CSS 类生成器、i18n 键匹配器和事件名称系统都受益于字符串值的编译时验证。语法很直观——你写一个带有 ${} 占位符的模式,TypeScript 验证实际值是否匹配该模式。

品牌类型解决了一个不同的问题。TypeScript 是结构类型系统,这意味着两个具有相同形状的类型是可以互换的。这通常是一个特性,但当你拥有的 ID 都是字符串但代表不同的实体时,这变得危险。UserId 和 OrderId 不应该可以互换,即使两者都是字符串。与 { __brand: "X" } 的交集创建了一个仅在编译时存在的虚类型——它没有运行时成本。

品牌类型是 TypeScript 在不使用额外工具的情况下最接近名义类型的做法。一次与虚属性的交集就能防止将错误 ID 传递给错误函数的那类错误。

satisfies 操作符——不牺牲推断

在 satisfies 之前,TypeScript 开发者在定义需要匹配类型同时保留其字面量值的常量时面临两难。你可以注解类型并失去窄推断,或者省略注解并失去验证。satisfies 操作符完全消除了这个权衡。

type ColorPalette = {
  primary: string;
  secondary: string;
  accent: string;
};

// Before satisfies — loses literal types
const paletteOld: ColorPalette = {
  primary: "#0f0f0f",  // type is string, not "#0f0f0f"
  secondary: "#ffffff",
  accent: "#0055ff",
};

// After satisfies — validates shape, keeps literals
const palette = {
  primary: "#0f0f0f",  // type is "#0f0f0f"
  secondary: "#ffffff",
  accent: "#0055ff",
} satisfies ColorPalette;

// palette.primary; // type is "#0f0f0f", not string

// Works with complex types too
type EventMap = {
  click: { x: number; y: number };
  focus: { target: string };
  input: { value: string };
};

const handlers = {
  click: (e: { x: number; y: number }) => console.log(e.x, e.y),
  focus: (e: { target: string }) => console.log(e.target),
} satisfies Partial<Record<keyof EventMap, Function>>;

// handlers.click — retains the parameter type from the implementation
// handlers.input — missing, but satisfies allows Partial
// Add input handler later without breaking anything

satisfies 操作符在配置对象、事件处理器和映射结构中最为有价值,在这些地方你希望对形状有类型安全性,但需要对值有尽可能窄的类型。它用单个关键字取代了一堆变通方法——结合类型注解的 as const 断言、冗余的类型转换和单独的验证函数。

泛型模式和类型安全的 API 客户端

TypeScript 中的泛型在简单情况下很容易使用——Array<T>、Promise<T>——但当你在单个函数签名中结合约束、条件类型和推断时,它们会变得真正强大。高级泛型最实用的应用是构建类型安全的 API 客户端,消除整类运行时错误。

// Type-safe API client
import { z } from "zod";

// Infer the output type from a Zod schema
type InferSchema<T extends z.ZodTypeAny> = T["_output"];

// Route definitions with path params and response schema
type RouteDef = {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  params?: Record<string, string>;
  body?: unknown;
};

class ApiClient {
  constructor(private base: string) {}

  get<
    TPath extends string,
    TSchema extends z.ZodTypeAny,
  >(
    path: TPath,
    schema: TSchema,
    ...[params]: TPath extends `${string}:${infer _}/${string}`
      ? [Record<string, string>]
      : TPath extends `${string}:${infer _}`
        ? [Record<string, string>]
        : [Record<string, string>?]
  ): Promise<InferSchema<TSchema>> {
    const resolved = params
      ? Object.entries(params).reduce(
          (p, [k, v]) => p.replace(`:${k}`, v),
          path
        )
      : path;
    return fetch(`${this.base}${resolved}`)
      .then((r) => r.json())
      .then((d) => schema.parse(d) as InferSchema<TSchema>);
  }
}

const api = new ApiClient("https://api.example.com");

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

// api.get("/users/:id", userSchema, { id: "123" });
// Result type: { id: string; name: string; email: string }
// If params are required but missing, type error at compile time

ApiClient 模式结合了几种高级技术。带有 infer 的条件类型检测路由是否包含路径参数,并有条件地要求 params 参数。响应模式在运行时用 Zod 解析并在编译时推断,所以返回类型始终正确。如果 API 更改了其响应形状,你更新模式,编译器会找到每个被破坏的使用方。

这种模式可以扩展到数百个端点而不需要仪式。每个端点只是一个路径字符串和一个 Zod 模式。泛型基础设施处理其余的——路径参数解析、响应验证和类型推断。使用这种模式的团队报告称消除了前后端之间的大部分集成错误。

  • 将 Zod 或 ArkType 模式与通用 fetch 包装器结合,实现跨网络边界的端到端类型安全。
  • 使用带有 infer 的条件类型,根据路由结构使参数成为必需或可选。
  • 从你的 API 客户端返回品牌类型,以便调用者不会意外地在不同实体类型之间交换 ID。

错误处理模式和模块增强

错误处理是大多数 TypeScript 代码库回退到 any 并假装问题不存在的地方。Result 模式——受 Rust 和函数式编程启发——将错误带入类型系统,这样编译器强制每个错误路径都被处理。模块增强将此方法扩展到第三方类型,让你为那些提供了不完整或过于宽松的类型定义的库增加类型安全性。

// Result type — errors are part of the return type, not thrown

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

async function fetchUser(id: UserId): Promise<Result<User, ApiError>> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return { ok: false, error: await ApiError.fromResponse(res) };
    }
    return { ok: true, value: await res.json() };
  } catch (err) {
    return { ok: false, error: new ApiError("network", String(err)) };
  }
}

// Consumer must handle both branches — compiler enforces it
const result = await fetchUser(userId);
if (result.ok) {
  console.log(result.value.name); // result.value is User
} else {
  console.error(result.error.code); // result.error is ApiError
}

// --- Module augmentation for third-party types ---

// express-session types are incomplete — augment them
declare module "express-session" {
  interface SessionData {
    userId?: string;
    role: "admin" | "user" | "viewer";
    permissions: string[];
  }
}

// Now req.session.role narrows to "admin" | "user" | "viewer"
// Without augmentation, req.session.role would be any

Result 模式在每个调用点强制显式错误处理。没有办法静默忽略一个失败的操作——编译器要求你在访问 result.value 之前检查 result.ok。这消除了被遗忘的 try-catch,这是无数生产事故的根源。代价是在每个调用点多打一些字,但好处是没有错误不被处理。

模块增强填补了第三方类型定义不足的空白。许多流行的库附带了过于宽松的类型——返回 any 的函数、类型化为 object 的参数或接口上缺少的属性。不要使用 as 进行转换或使用 @ts-ignore,在 .d.ts 或 .ts 文件中 declare module "library-name" 并添加缺少的类型。声明会自动合并,你整个代码库都会从修复中受益。

这些模式的组合——严格配置、可辨识联合、模板字面量类型、品牌类型、satisfies、类型安全泛型和显式错误处理——代表了 2026 年 TypeScript 的全面方法。每个模式都独立有用,但组合起来它们创建了一个编译器能够捕获否则会成为生产事故的错误的代码库。学习这些模式的投资会在减少调试时间、更安全的重构和真正难以误用的 API 方面得到多倍回报。