typescript-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript Best Practices

TypeScript最佳实践

Pair with React Best Practices

搭配React最佳实践使用

When working with React components (
.tsx
,
.jsx
files or
@react
imports), always load
react-best-practices
alongside this skill. This skill covers TypeScript fundamentals; React-specific patterns (effects, hooks, refs, component design) are in the dedicated React skill.
在处理React组件(
.tsx
.jsx
文件或
@react
导入)时,请始终同时加载
react-best-practices
技能。本技能涵盖TypeScript基础;React特定模式(副作用、hooks、refs、组件设计)在专门的React技能中介绍。

Type-First Development

类型优先开发

Types define the contract before implementation. Follow this workflow:
  1. Define the data model - types, interfaces, and schemas first
  2. Define function signatures - input/output types before logic
  3. Implement to satisfy types - let the compiler guide completeness
  4. Validate at boundaries - runtime checks where data enters the system
类型在实现前定义契约。遵循以下工作流程:
  1. 定义数据模型 - 先定义类型、接口和模式
  2. 定义函数签名 - 先定义输入/输出类型再写逻辑
  3. 按类型要求实现 - 让编译器引导代码完整性
  4. 在边界处验证 - 在数据进入系统的位置进行运行时检查

Make Illegal States Unrepresentable

实现非法状态不可表示

Use the type system to prevent invalid states at compile time.
Discriminated unions for mutually exclusive states:
ts
// Good: only valid combinations possible
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

// Bad: allows invalid combinations like { loading: true, error: Error }
type RequestState<T> = {
  loading: boolean;
  data?: T;
  error?: Error;
};
Branded types for domain primitives:
ts
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

// Compiler prevents passing OrderId where UserId expected
function getUser(id: UserId): Promise<User> { /* ... */ }

function createUserId(id: string): UserId {
  return id as UserId;
}
Const assertions for literal unions:
ts
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'

// Array and type stay in sync automatically
function isValidRole(role: string): role is Role {
  return ROLES.includes(role as Role);
}
Required vs optional fields - be explicit:
ts
// Creation: some fields required
type CreateUser = {
  email: string;
  name: string;
};

// Update: all fields optional
type UpdateUser = Partial<CreateUser>;

// Database row: all fields present
type User = CreateUser & {
  id: UserId;
  createdAt: Date;
};
利用类型系统在编译时防止无效状态。
互斥状态的区分联合类型:
ts
// Good: only valid combinations possible
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

// Bad: allows invalid combinations like { loading: true, error: Error }
type RequestState<T> = {
  loading: boolean;
  data?: T;
  error?: Error;
};
领域原语的品牌类型:
ts
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

// Compiler prevents passing OrderId where UserId expected
function getUser(id: UserId): Promise<User> { /* ... */ }

function createUserId(id: string): UserId {
  return id as UserId;
}
字面量联合的const断言:
ts
const ROLES = ['admin', 'user', 'guest'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'

// Array and type stay in sync automatically
function isValidRole(role: string): role is Role {
  return ROLES.includes(role as Role);
}
必填与可选字段 - 显式声明:
ts
// Creation: some fields required
type CreateUser = {
  email: string;
  name: string;
};

// Update: all fields optional
type UpdateUser = Partial<CreateUser>;

// Database row: all fields present
type User = CreateUser & {
  id: UserId;
  createdAt: Date;
};

Module Structure

模块结构

Prefer smaller, focused files: one component, hook, or utility per file. Split when a file handles multiple concerns or exceeds ~200 lines. Colocate tests with implementation (
foo.test.ts
alongside
foo.ts
). Group related files by feature rather than by type.
优先使用更小、聚焦的文件:每个文件对应一个组件、hook或工具函数。当一个文件处理多个关注点或超过约200行时,进行拆分。将测试文件与实现文件放在同一目录下(
foo.test.ts
foo.ts
相邻)。按功能而非类型对相关文件进行分组。

Functional Patterns

函数式模式

  • Prefer
    const
    over
    let
    ; use
    readonly
    and
    Readonly<T>
    for immutable data.
  • Use
    array.map/filter/reduce
    over
    for
    loops; chain transformations in pipelines.
  • Write pure functions for business logic; isolate side effects in dedicated modules.
  • Avoid mutating function parameters; return new objects/arrays instead.
  • 优先使用
    const
    而非
    let
    ;对不可变数据使用
    readonly
    Readonly<T>
  • 使用
    array.map/filter/reduce
    而非
    for
    循环;通过链式调用实现数据转换流水线。
  • 为业务逻辑编写纯函数;将副作用隔离在专用模块中。
  • 避免修改函数参数;返回新的对象/数组替代。

Instructions

注意事项

  • Enable
    strict
    mode; model data with interfaces and types. Strong typing catches bugs at compile time.
  • Every code path returns a value or throws; use exhaustive
    switch
    with
    never
    checks in default. Unhandled cases become compile errors.
  • Propagate errors with context; catching requires re-throwing or returning a meaningful result. Hidden failures delay debugging.
  • Handle edge cases explicitly: empty arrays, null/undefined inputs, boundary values. Defensive checks prevent runtime surprises.
  • Use
    await
    for async calls; wrap external calls with contextual error messages. Unhandled rejections crash Node processes.
  • Add or update focused tests when changing logic; test behavior, not implementation details.
  • 启用
    strict
    模式;使用接口和类型建模数据。强类型可在编译时捕获bug。
  • 每个代码路径要么返回值要么抛出异常;在
    switch
    语句的默认分支中使用
    never
    检查实现穷举处理。未处理的情况会成为编译错误。
  • 携带上下文传播错误;捕获异常后需要重新抛出或返回有意义的结果。隐藏的故障会延迟调试。
  • 显式处理边缘情况:空数组、null/undefined输入、边界值。防御性检查可避免运行时意外。
  • 对异步调用使用
    await
    ;为外部调用添加上下文错误信息。未处理的拒绝会导致Node进程崩溃。
  • 修改逻辑时添加或更新针对性测试;测试行为而非实现细节。

Examples

示例

Explicit failure for unimplemented logic:
ts
export function buildWidget(widgetType: string): never {
  throw new Error(`buildWidget not implemented for type: ${widgetType}`);
}
Exhaustive switch with never check:
ts
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}`);
    }
  }
}
Wrap external calls with context:
ts
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();
}
Debug logging with namespaced logger:
ts
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;
}
未实现逻辑的显式失败:
ts
export function buildWidget(widgetType: string): never {
  throw new Error(`buildWidget not implemented for type: ${widgetType}`);
}
带never检查的穷举switch:
ts
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}`);
    }
  }
}
为外部调用添加上下文:
ts
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();
}
带命名空间的调试日志:
ts
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;
}

Runtime Validation with Zod

使用Zod进行运行时验证

  • Define schemas as single source of truth; infer TypeScript types with
    z.infer<>
    . Avoid duplicating types and schemas.
  • Use
    safeParse
    for user input where failure is expected; use
    parse
    at trust boundaries where invalid data is a bug.
  • Compose schemas with
    .extend()
    ,
    .pick()
    ,
    .omit()
    ,
    .merge()
    for DRY definitions.
  • Add
    .transform()
    for data normalization at parse time (trim strings, parse dates).
  • Include descriptive error messages; use
    .refine()
    for custom validation logic.
  • 将模式定义作为唯一可信源;使用
    z.infer<>
    推导TypeScript类型。避免重复定义类型和模式。
  • 对预期可能失败的用户输入使用
    safeParse
    ;在信任边界处(无效数据属于bug的场景)使用
    parse
  • 使用
    .extend()
    .pick()
    .omit()
    .merge()
    组合模式,实现DRY(不重复自己)的定义。
  • 使用
    .transform()
    在解析时进行数据归一化(修剪字符串、解析日期)。
  • 添加描述性错误信息;使用
    .refine()
    实现自定义验证逻辑。

Examples

示例

Schema as source of truth with type inference:
ts
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>;
Return parse results to callers (never swallow errors):
ts
import { z, SafeParseReturnType } from "zod";

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

// Caller handles both success and error:
const result = parseUserInput(formData);
if (!result.success) {
  setErrors(result.error.flatten().fieldErrors);
  return;
}
await submitUser(result.data);
Strict parsing at trust boundaries:
ts
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); // throws if API contract violated
}
Schema composition:
ts
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = CreateUserSchema.partial();
const UserWithPostsSchema = UserSchema.extend({
  posts: z.array(PostSchema),
});
作为可信源的模式与类型推导:
ts
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>;
将解析结果返回给调用方(绝不吞掉错误):
ts
import { z, SafeParseReturnType } from "zod";

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

// Caller handles both success and error:
const result = parseUserInput(formData);
if (!result.success) {
  setErrors(result.error.flatten().fieldErrors);
  return;
}
await submitUser(result.data);
信任边界处的严格解析:
ts
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); // throws if API contract violated
}
模式组合:
ts
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = CreateUserSchema.partial();
const UserWithPostsSchema = UserSchema.extend({
  posts: z.array(PostSchema),
});

Configuration

配置

  • Load config from environment variables at startup; validate with Zod before use. Invalid config should crash immediately.
  • Define a typed config object as single source of truth; avoid accessing
    process.env
    throughout the codebase.
  • Use sensible defaults for development; require explicit values for production secrets.
  • 在启动时从环境变量加载配置;使用Zod验证后再使用。无效配置应立即导致程序崩溃。
  • 将类型化的配置对象作为唯一可信源;避免在代码库中直接访问
    process.env
  • 为开发环境使用合理的默认值;为生产环境的密钥要求显式配置。

Examples

示例

Typed config with Zod validation:
ts
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);
Access config values (not process.env directly):
ts
import { config } from "./config";

const server = app.listen(config.PORT);
const db = connect(config.DATABASE_URL);
带Zod验证的类型化配置:
ts
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):
ts
import { config } from "./config";

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

Optional: type-fest

可选工具:type-fest

For advanced type utilities beyond TypeScript builtins, consider type-fest:
  • Opaque<T, Token>
    - cleaner branded types than manual
    & { __brand }
    pattern
  • PartialDeep<T>
    - recursive partial for nested objects
  • ReadonlyDeep<T>
    - recursive readonly for immutable data
  • LiteralUnion<Literals, Fallback>
    - literals with autocomplete + string fallback
  • SetRequired<T, K>
    /
    SetOptional<T, K>
    - targeted field modifications
  • Simplify<T>
    - flatten complex intersection types in IDE tooltips
ts
import type { Opaque, PartialDeep, SetRequired } from 'type-fest';

// Branded type (cleaner than manual approach)
type UserId = Opaque<string, 'UserId'>;

// Deep partial for patch operations
type UserPatch = PartialDeep<User>;

// Make specific fields required
type UserWithEmail = SetRequired<Partial<User>, 'email'>;
对于TypeScript内置工具之外的高级类型工具,可以考虑type-fest
  • Opaque<T, Token>
    - 比手动
    & { __brand }
    模式更简洁的品牌类型
  • PartialDeep<T>
    - 嵌套对象的递归部分类型
  • ReadonlyDeep<T>
    - 不可变数据的递归只读类型
  • LiteralUnion<Literals, Fallback>
    - 带自动补全的字面量联合类型 + 字符串回退
  • SetRequired<T, K>
    /
    SetOptional<T, K>
    - 针对性的字段修改
  • Simplify<T>
    - 在IDE提示中展平复杂的交叉类型
ts
import type { Opaque, PartialDeep, SetRequired } from 'type-fest';

// Branded type (cleaner than manual approach)
type UserId = Opaque<string, 'UserId'>;

// Deep partial for patch operations
type UserPatch = PartialDeep<User>;

// Make specific fields required
type UserWithEmail = SetRequired<Partial<User>, 'email'>;