name: typescript-best-practices description: 提供TypeScript类型优先开发的模式,使非法状态无法表示,实现穷尽性处理与运行时验证。在读取或写入TypeScript/JavaScript文件时必须使用。
TypeScript 最佳实践
与 React 最佳实践配合使用
当处理 React 组件(.tsx、.jsx 文件或 @react 导入)时,请始终与此技能一同加载 react-best-practices。本技能涵盖 TypeScript 基础;React 特定模式(副作用、钩子、引用、组件设计)在专门的 React 技能中。
类型优先开发
类型在实现之前定义契约。遵循以下工作流程:
- 定义数据模型 - 首先定义类型、接口和模式
- 定义函数签名 - 在逻辑之前定义输入/输出类型
- 实现以满足类型 - 让编译器指导完整性
- 在边界处验证 - 在数据进入系统时进行运行时检查
使非法状态无法表示
使用类型系统在编译时防止无效状态。
用于互斥状态的判别联合类型:
// 好:只允许有效的组合
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.ts 与 foo.ts 放在一起)。按功能而非类型对相关文件进行分组。
函数式模式
- 优先使用
const而非let;对不可变数据使用readonly和Readonly<T>。 - 使用
array.map/filter/reduce而非for循环;在管道中链接转换。 - 为业务逻辑编写纯函数;将副作用隔离在专用模块中。
- 避免改变函数参数;返回新的对象/数组。
指令
- 启用
strict模式;使用接口和类型对数据进行建模。强类型在编译时捕获错误。 - 每个代码路径都返回值或抛出异常;在默认分支中使用穷尽的
switch和never检查。未处理的情况会成为编译错误。 - 传播带有上下文的错误;捕获错误需要重新抛出或返回有意义的结果。隐藏的失败会延迟调试。
- 明确处理边缘情况:空数组、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>- 用于嵌套对象的递归 PartialReadonlyDeep<T>- 用于不可变数据的递归 ReadonlyLiteralUnion<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'>;