TypeScript最佳实践 typescript-best-practices

TypeScript最佳实践指南,专注于类型优先开发、非法状态预防、穷尽性处理和运行时验证。包含Zod模式验证、函数式编程、模块化设计和配置管理。适用于TypeScript/JavaScript开发者提升代码质量和类型安全性。关键词:TypeScript, 类型安全, 最佳实践, Zod, 运行时验证, 函数式编程, 类型优先开发

前端开发 0 次安装 0 次浏览 更新于 2/23/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'>;