TypeScript最佳实践 typescript-best-practices

本技能提供一套完整的 TypeScript 开发最佳实践指南,专注于类型优先开发、编译时安全、运行时验证和健壮代码编写。核心内容包括:使用类型系统使非法状态无法表示,通过判别联合、标记类型和常量断言提升代码安全性;采用模块化、函数式编程模式;结合 Zod 库进行运行时数据验证与配置管理。适用于所有 TypeScript/JavaScript 项目,旨在帮助开发者编写更可靠、易维护且错误更少的代码。关键词:TypeScript 最佳实践,类型优先开发,Zod 验证,编译时安全,代码健壮性,前端开发,JavaScript 类型安全。

前端开发 0 次安装 0 次浏览 更新于 2/28/2026

name: typescript-best-practices description: 提供TypeScript类型优先开发的模式,使非法状态无法表示,实现穷尽性处理与运行时验证。在读取或写入TypeScript/JavaScript文件时必须使用。

TypeScript 最佳实践

与 React 最佳实践配合使用

当处理 React 组件(.tsx.jsx 文件或 @react 导入)时,请始终与此技能一同加载 react-best-practices。本技能涵盖 TypeScript 基础;React 特定模式(副作用、钩子、引用、组件设计)在专门的 React 技能中。

类型优先开发

类型在实现之前定义契约。遵循以下工作流程:

  1. 定义数据模型 - 首先定义类型、接口和模式
  2. 定义函数签名 - 在逻辑之前定义输入/输出类型
  3. 实现以满足类型 - 让编译器指导完整性
  4. 在边界处验证 - 在数据进入系统时进行运行时检查

使非法状态无法表示

使用类型系统在编译时防止无效状态。

用于互斥状态的判别联合类型:

// 好:只允许有效的组合
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

// 差:允许无效组合,如 { loading: true, error: Error }
type RequestState<T> = {
  loading: boolean;
  data?: T;
  error?: Error;
};

用于领域原语的标记类型:

type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

// 编译器阻止在需要 UserId 的地方传递 OrderId
function getUser(id: UserId): Promise<User> { /* ... */ }

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

用于字面量联合类型的常量断言:

const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'

// 数组和类型自动保持同步
function isValidRole(role: string): role is Role {
  return ROLES.includes(role as Role);
}

必需字段与可选字段 - 明确指定:

// 创建:某些字段必需
type CreateUser = {
  email: string;
  name: string;
};

// 更新:所有字段可选
type UpdateUser = Partial<CreateUser>;

// 数据库行:所有字段都存在
type User = CreateUser & {
  id: UserId;
  createdAt: Date;
};

模块结构

优先使用更小、专注的文件:每个文件一个组件、钩子或工具。当一个文件处理多个关注点或超过约 200 行时进行拆分。将测试与实现放在一起(foo.test.tsfoo.ts 放在一起)。按功能而非类型对相关文件进行分组。

函数式模式

  • 优先使用 const 而非 let;对不可变数据使用 readonlyReadonly<T>
  • 使用 array.map/filter/reduce 而非 for 循环;在管道中链接转换。
  • 为业务逻辑编写纯函数;将副作用隔离在专用模块中。
  • 避免改变函数参数;返回新的对象/数组。

指令

  • 启用 strict 模式;使用接口和类型对数据进行建模。强类型在编译时捕获错误。
  • 每个代码路径都返回值或抛出异常;在默认分支中使用穷尽的 switchnever 检查。未处理的情况会成为编译错误。
  • 传播带有上下文的错误;捕获错误需要重新抛出或返回有意义的结果。隐藏的失败会延迟调试。
  • 明确处理边缘情况:空数组、null/undefined 输入、边界值。防御性检查防止运行时意外。
  • 对异步调用使用 await;用上下文错误消息包装外部调用。未处理的拒绝会导致 Node 进程崩溃。
  • 更改逻辑时添加或更新专注的测试;测试行为,而非实现细节。

示例

对未实现逻辑的显式失败:

export function buildWidget(widgetType: string): never {
  throw new Error(`buildWidget not implemented for type: ${widgetType}`);
}

带有 never 检查的穷尽 switch:

type Status = "active" | "inactive";

export function processStatus(status: Status): string {
  switch (status) {
    case "active":
      return "processing";
    case "inactive":
      return "skipped";
    default: {
      const _exhaustive: never = status;
      throw new Error(`unhandled status: ${_exhaustive}`);
    }
  }
}

用上下文包装外部调用:

export async function fetchWidget(id: string): Promise<Widget> {
  const response = await fetch(`/api/widgets/${id}`);
  if (!response.ok) {
    throw new Error(`fetch widget ${id} failed: ${response.status}`);
  }
  return response.json();
}

使用命名空间记录器的调试日志:

import debug from "debug";

const log = debug("myapp:widgets");

export function createWidget(name: string): Widget {
  log("creating widget: %s", name);
  const widget = { id: crypto.randomUUID(), name };
  log("created widget: %s", widget.id);
  return widget;
}

使用 Zod 进行运行时验证

  • 将模式定义为单一事实来源;使用 z.infer<> 推断 TypeScript 类型。避免重复类型和模式。
  • 对于预期会失败的用户输入使用 safeParse;在信任边界处使用 parse,其中无效数据被视为错误。
  • 使用 .extend().pick().omit().merge() 组合模式以实现 DRY 定义。
  • 添加 .transform() 用于在解析时进行数据规范化(修剪字符串、解析日期)。
  • 包含描述性错误消息;使用 .refine() 进行自定义验证逻辑。

示例

作为单一事实来源的模式与类型推断:

import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1),
  createdAt: z.string().transform((s) => new Date(s)),
});

type User = z.infer<typeof UserSchema>;

将解析结果返回给调用者(绝不吞掉错误):

import { z, SafeParseReturnType } from "zod";

export function parseUserInput(raw: unknown): SafeParseReturnType<unknown, User> {
  return UserSchema.safeParse(raw);
}

// 调用者处理成功和错误:
const result = parseUserInput(formData);
if (!result.success) {
  setErrors(result.error.flatten().fieldErrors);
  return;
}
await submitUser(result.data);

在信任边界处的严格解析:

export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`fetch user ${id} failed: ${response.status}`);
  }
  const data = await response.json();
  return UserSchema.parse(data); // 如果违反 API 契约则抛出异常
}

模式组合:

const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = CreateUserSchema.partial();
const UserWithPostsSchema = UserSchema.extend({
  posts: z.array(PostSchema),
});

配置

  • 在启动时从环境变量加载配置;在使用前使用 Zod 进行验证。无效配置应立即导致崩溃。
  • 将类型化的配置对象定义为单一事实来源;避免在整个代码库中访问 process.env
  • 为开发环境使用合理的默认值;对生产环境的密钥要求显式值。

示例

使用 Zod 验证的类型化配置:

import { z } from "zod";

const ConfigSchema = z.object({
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
});

export const config = ConfigSchema.parse(process.env);

访问配置值(不直接访问 process.env):

import { config } from "./config";

const server = app.listen(config.PORT);
const db = connect(config.DATABASE_URL);

可选:type-fest

对于超越 TypeScript 内置功能的高级类型工具,请考虑 type-fest

  • Opaque<T, Token> - 比手动 & { __brand } 模式更简洁的标记类型
  • PartialDeep<T> - 用于嵌套对象的递归 Partial
  • ReadonlyDeep<T> - 用于不可变数据的递归 Readonly
  • LiteralUnion<Literals, Fallback> - 具有自动完成功能 + 字符串回退的字面量
  • SetRequired<T, K> / SetOptional<T, K> - 针对性的字段修改
  • Simplify<T> - 在 IDE 工具提示中展平复杂的交叉类型
import type { Opaque, PartialDeep, SetRequired } from 'type-fest';

// 标记类型(比手动方法更简洁)
type UserId = Opaque<string, 'UserId'>;

// 用于补丁操作的深度 Partial
type UserPatch = PartialDeep<User>;

// 使特定字段成为必需
type UserWithEmail = SetRequired<Partial<User>, 'email'>;