typescript-guide

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript/JavaScript Guide

TypeScript/JavaScript开发指南

Applies to: TypeScript 5+, Node.js 20+, ES2022+, React, Server-Side JS
适用范围:TypeScript 5+、Node.js 20+、ES2022+、React、服务端JS

Core Principles

核心原则

  1. Strict TypeScript: Enable all strict flags; treat type errors as build failures
  2. Immutability by Default: Use
    const
    ,
    readonly
    ,
    as const
    , and spread operators; mutate only when profiling demands it
  3. Explicit Types at Boundaries: All function signatures, API responses, and public interfaces must have explicit type annotations; infer internally
  4. Functional Patterns: Prefer pure functions, map/filter/reduce, and composition over classes and mutation
  5. Zero
    any
    : Use
    unknown
    for truly unknown data, Zod/io-ts for runtime narrowing; every
    any
    requires a code-review comment explaining why
  1. 严格TypeScript配置:开启所有严格模式标识,将类型错误视为构建失败
  2. 默认不可变:使用
    const
    readonly
    as const
    和展开运算符,仅当性能分析明确要求时才执行可变操作
  3. 边界显式类型:所有函数签名、API响应和公共接口必须有显式类型注解,内部逻辑可使用类型推断
  4. 函数式编程模式:优先使用纯函数、map/filter/reduce和组合式写法,而非类和可变操作
  5. any
    使用
    :对于完全未知的数据使用
    unknown
    ,使用Zod/io-ts做运行时类型收窄,每一处
    any
    都需要在代码评审注释中说明原因

Guardrails

规范约束

TypeScript Configuration

TypeScript配置

  • Enable
    "strict": true
    (this activates
    strictNullChecks
    ,
    noImplicitAny
    ,
    strictFunctionTypes
    , etc.)
  • Enable
    "noUncheckedIndexedAccess": true
    (arrays and records return
    T | undefined
    )
  • Enable
    "noImplicitReturns": true
    and
    "noFallthroughCasesInSwitch": true
  • Set
    "target": "ES2022"
    ,
    "module": "ESNext"
    ,
    "moduleResolution": "bundler"
  • Never suppress errors with
    @ts-ignore
    ; use
    @ts-expect-error
    with a justification comment
  • 开启
    "strict": true
    (会自动启用
    strictNullChecks
    noImplicitAny
    strictFunctionTypes
    等规则)
  • 开启
    "noUncheckedIndexedAccess": true
    (数组和record返回类型为
    T | undefined
  • 开启
    "noImplicitReturns": true
    "noFallthroughCasesInSwitch": true
  • 设置
    "target": "ES2022"
    "module": "ESNext"
    "moduleResolution": "bundler"
  • 禁止使用
    @ts-ignore
    屏蔽错误,需使用
    @ts-expect-error
    并附带说明注释

Code Style

代码风格

  • const
    over
    let
    ; never use
    var
  • Nullish coalescing (
    ??
    ) over logical OR (
    ||
    ) for defaults (avoids falsy-value bugs with
    0
    ,
    ""
    ,
    false
    )
  • Optional chaining (
    ?.
    ) over nested null checks
  • Template literals over string concatenation
  • Destructuring at call sites:
    const { id, name } = user
  • Barrel exports (
    index.ts
    ) only at package boundaries, not within internal modules (causes circular imports and tree-shaking failures)
  • Enums: prefer
    as const
    objects over TypeScript
    enum
    (enums produce runtime code and have surprising behaviors with reverse mappings)
typescript
// Prefer this
const Status = {
  Active: "active",
  Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];

// Over this
enum Status {
  Active = "active",
  Inactive = "inactive",
}
  • 优先使用
    const
    而非
    let
    ,禁止使用
    var
  • 空值合并运算符(
    ??
    )优先于逻辑或(
    ||
    )设置默认值(避免
    0
    ""
    false
    这类假值引发的bug)
  • 可选链(
    ?.
    )优先于多层嵌套空值检查
  • 模板字面量优先于字符串拼接
  • 调用侧使用解构:
    const { id, name } = user
  • 仅在包边界使用桶导出(
    index.ts
    ),内部模块禁止使用(会导致循环依赖和tree-shaking失效)
  • 枚举:优先使用
    as const
    对象而非TypeScript原生
    enum
    (枚举会生成运行时代码,且反向映射存在不符合预期的行为)
typescript
// 推荐写法
const Status = {
  Active: "active",
  Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];

// 不推荐写法
enum Status {
  Active = "active",
  Inactive = "inactive",
}

Error Handling

错误处理

  • Every
    catch
    block must type-narrow the error before accessing properties
  • Never throw primitive values (strings, numbers); always throw
    Error
    subclasses
  • Use the
    cause
    option for error chaining:
    new AppError("msg", { cause: original })
  • Never swallow errors with empty
    catch
    blocks
  • Async functions must have error handling at the call site or a global boundary handler
  • 每个
    catch
    块在访问属性前必须对错误做类型收窄
  • 禁止抛出原始值(字符串、数字),始终抛出
    Error
    子类
  • 使用
    cause
    参数做错误链传递:
    new AppError("msg", { cause: original })
  • 禁止使用空
    catch
    块吞掉错误
  • 异步函数必须在调用侧处理错误,或在全局边界统一处理

Async/Await

Async/Await

  • Always
    await
    or return promises; never create fire-and-forget promises without explicit
    void
    annotation
  • Use
    Promise.all
    for independent concurrent operations, not sequential
    await
    in a loop
  • Set timeouts on all external calls (fetch, database, third-party APIs) using
    AbortController
  • Use
    Promise.allSettled
    when partial failure is acceptable
  • Never mix
    .then()
    chains with
    await
    in the same function
typescript
// Bad: sequential when order does not matter
const users = await fetchUsers();
const posts = await fetchPosts();

// Good: concurrent
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);

// Good: timeout with AbortController
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), ms);
  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timeout);
  }
}
  • 始终对promise执行
    await
    或返回,禁止创建即发即弃的promise,除非显式标注
    void
  • 独立的并发操作使用
    Promise.all
    ,而非循环中顺序
    await
  • 所有外部调用(fetch、数据库、第三方API)都需要使用
    AbortController
    设置超时
  • 允许部分失败的场景使用
    Promise.allSettled
  • 同一个函数内禁止混合使用
    .then()
    链式调用和
    await
typescript
// 坏示例:顺序不相关时仍串行执行
const users = await fetchUsers();
const posts = await fetchPosts();

// 好示例:并发执行
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);

// 好示例:使用AbortController设置超时
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), ms);
  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timeout);
  }
}

Module System

模块系统

  • Use ES modules (
    import
    /
    export
    ) exclusively; no
    require()
    in TypeScript
  • One export per concept: prefer named exports over default exports (improves refactoring, grep-ability)
  • Side-effect imports (
    import "./setup"
    ) must be documented with a comment explaining why
  • Re-export from
    index.ts
    only at package/feature boundaries
  • Keep import order: stdlib/node builtins, external packages, internal modules, relative paths (enforce with ESLint
    import/order
    )
  • 仅使用ES模块(
    import
    /
    export
    ),TypeScript中禁止使用
    require()
  • 每个概念单独导出:优先使用命名导出而非默认导出(提升重构效率和可检索性)
  • 副作用导入(
    import "./setup"
    )必须附带注释说明原因
  • 仅在包/功能模块边界使用
    index.ts
    做重新导出
  • 保持导入顺序:标准库/Node内置模块、第三方依赖、内部模块、相对路径(可通过ESLint的
    import/order
    规则强制)

Project Structure

项目结构

myproject/
├── src/
│   ├── index.ts              # Application entry point
│   ├── config/               # Environment, feature flags
│   │   └── env.ts            # Validated env vars (Zod schema)
│   ├── domain/               # Business logic (no I/O)
│   │   ├── user.ts
│   │   └── order.ts
│   ├── services/             # Application services (orchestrate domain + I/O)
│   ├── repositories/         # Data access (database, external APIs)
│   ├── routes/               # HTTP handlers / controllers
│   ├── middleware/            # Express/Koa/Hono middleware
│   ├── utils/                # Pure utility functions
│   └── types/                # Shared type definitions
├── tests/
│   ├── unit/                 # Mirror src/ structure
│   ├── integration/          # API and database tests
│   └── fixtures/             # Shared test data
├── tsconfig.json
├── package.json
├── .eslintrc.cjs
├── .prettierrc
└── vitest.config.ts
  • Domain logic in
    domain/
    must have zero I/O dependencies (pure functions, easy to test)
  • No business logic in route handlers; delegate to services
  • Shared types in
    types/
    ; co-locate component-specific types with their module
myproject/
├── src/
│   ├── index.ts              # 应用入口
│   ├── config/               # 环境变量、功能开关
│   │   └── env.ts            # 校验后的环境变量(Zod schema)
│   ├── domain/               # 业务逻辑(无I/O依赖)
│   │   ├── user.ts
│   │   └── order.ts
│   ├── services/             # 应用服务(编排领域逻辑 + I/O操作)
│   ├── repositories/         # 数据访问层(数据库、外部API)
│   ├── routes/               # HTTP处理器/控制器
│   ├── middleware/            # Express/Koa/Hono中间件
│   ├── utils/                # 纯工具函数
│   └── types/                # 共享类型定义
├── tests/
│   ├── unit/                 # 单元测试,目录结构与src/对齐
│   ├── integration/          # API和数据库集成测试
│   └── fixtures/             # 共享测试数据
├── tsconfig.json
├── package.json
├── .eslintrc.cjs
├── .prettierrc
└── vitest.config.ts
  • domain/
    下的领域逻辑必须无I/O依赖(纯函数,易于测试)
  • 路由处理器中不编写业务逻辑,统一委托给services处理
  • 共享类型放在
    types/
    下,组件专属类型与对应模块放在一起

Error Handling Patterns

错误处理模式

Custom Application Errors

自定义应用错误

typescript
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    options?: ErrorOptions
  ) {
    super(message, options);
    this.name = "AppError";
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, "NOT_FOUND", 404);
    this.name = "NotFoundError";
  }
}
typescript
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    options?: ErrorOptions
  ) {
    super(message, options);
    this.name = "AppError";
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`, "NOT_FOUND", 404);
    this.name = "NotFoundError";
  }
}

Result Pattern (for Expected Failures)

Result模式(用于预期失败场景)

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

function parseAge(input: string): Result<number> {
  const age = Number(input);
  if (Number.isNaN(age) || age < 0 || age > 150) {
    return { ok: false, error: new AppError("Invalid age", "VALIDATION") };
  }
  return { ok: true, value: age };
}

// Usage: caller must check before accessing value
const result = parseAge(input);
if (!result.ok) {
  return res.status(400).json({ error: result.error.message });
}
console.log(result.value); // Type-safe: number
typescript
type Result<T, E = AppError> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseAge(input: string): Result<number> {
  const age = Number(input);
  if (Number.isNaN(age) || age < 0 || age > 150) {
    return { ok: false, error: new AppError("Invalid age", "VALIDATION") };
  }
  return { ok: true, value: age };
}

// 用法:调用方访问值前必须做检查
const result = parseAge(input);
if (!result.ok) {
  return res.status(400).json({ error: result.error.message });
}
console.log(result.value); // 类型安全:number

Typed Catch Blocks

类型化Catch块

typescript
try {
  await externalService.call();
} catch (error: unknown) {
  if (error instanceof AppError) {
    logger.warn(error.message, { code: error.code });
    return res.status(error.statusCode).json({ error: error.message });
  }
  // Unknown error: wrap and re-throw
  throw new AppError("Unexpected failure", "INTERNAL", 500, { cause: error });
}
typescript
try {
  await externalService.call();
} catch (error: unknown) {
  if (error instanceof AppError) {
    logger.warn(error.message, { code: error.code });
    return res.status(error.statusCode).json({ error: error.message });
  }
  // 未知错误:包装后重新抛出
  throw new AppError("Unexpected failure", "INTERNAL", 500, { cause: error });
}

Testing

测试

Standards

标准

  • Test runner: Vitest (preferred) or Jest
  • Test files: co-located as
    *.test.ts
    or under
    tests/
    mirroring
    src/
  • Naming:
    describe("ModuleName")
    with
    it("should [expected behavior] when [condition]")
  • Use
    beforeEach
    for setup; avoid
    beforeAll
    for mutable state
  • Coverage target: >80% for business logic, >60% overall
  • No
    any
    in test files; test helpers must be typed
  • Mock at boundaries (HTTP, database, file system), not internal functions
  • 测试运行器:优先使用Vitest,也可使用Jest
  • 测试文件:可与业务代码放在一起命名为
    *.test.ts
    ,或放在
    tests/
    目录下与
    src/
    结构对齐
  • 命名规范:
    describe("模块名")
    ,测试用例命名为
    it("should [预期行为] when [触发条件]")
  • 使用
    beforeEach
    做测试准备,可变状态场景避免使用
    beforeAll
  • 覆盖率目标:业务逻辑覆盖率>80%,整体覆盖率>60%
  • 测试文件中禁止使用
    any
    ,测试辅助函数必须有类型定义
  • 仅在边界(HTTP、数据库、文件系统)做mock,不要mock内部函数

Table-Driven Tests

表驱动测试

typescript
import { describe, it, expect } from "vitest";
import { validateEmail } from "./validate";

describe("validateEmail", () => {
  const cases = [
    { input: "user@example.com", expected: true, desc: "valid email" },
    { input: "no-at-sign", expected: false, desc: "missing @ symbol" },
    { input: "", expected: false, desc: "empty string" },
    { input: "a@b.c", expected: true, desc: "minimal valid email" },
  ] as const;

  it.each(cases)("returns $expected for $desc", ({ input, expected }) => {
    expect(validateEmail(input)).toBe(expected);
  });
});
typescript
import { describe, it, expect } from "vitest";
import { validateEmail } from "./validate";

describe("validateEmail", () => {
  const cases = [
    { input: "user@example.com", expected: true, desc: "valid email" },
    { input: "no-at-sign", expected: false, desc: "missing @ symbol" },
    { input: "", expected: false, desc: "empty string" },
    { input: "a@b.c", expected: true, desc: "minimal valid email" },
  ] as const;

  it.each(cases)("returns $expected for $desc", ({ input, expected }) => {
    expect(validateEmail(input)).toBe(expected);
  });
});

Mocking External Dependencies

外部依赖模拟

typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserService } from "./user.service";
import type { UserRepository } from "./user.repository";

describe("UserService", () => {
  let service: UserService;
  let repo: UserRepository;

  beforeEach(() => {
    repo = {
      findById: vi.fn(),
      save: vi.fn(),
    } as unknown as UserRepository;
    service = new UserService(repo);
  });

  it("should throw NotFoundError when user does not exist", async () => {
    vi.mocked(repo.findById).mockResolvedValue(null);

    await expect(service.getUser("abc")).rejects.toThrow("not found");
    expect(repo.findById).toHaveBeenCalledWith("abc");
  });
});
typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserService } from "./user.service";
import type { UserRepository } from "./user.repository";

describe("UserService", () => {
  let service: UserService;
  let repo: UserRepository;

  beforeEach(() => {
    repo = {
      findById: vi.fn(),
      save: vi.fn(),
    } as unknown as UserRepository;
    service = new UserService(repo);
  });

  it("should throw NotFoundError when user does not exist", async () => {
    vi.mocked(repo.findById).mockResolvedValue(null);

    await expect(service.getUser("abc")).rejects.toThrow("not found");
    expect(repo.findById).toHaveBeenCalledWith("abc");
  });
});

Tooling

工具链

Essential Commands

常用命令

bash
tsc --noEmit                     # Type check (no output)
eslint . --ext .ts,.tsx          # Lint
prettier --check .               # Format check
prettier --write .               # Format fix
vitest                           # Run tests (watch mode)
vitest run                       # Run tests (CI mode)
vitest run --coverage            # With coverage
bash
tsc --noEmit                     # 类型检查(无输出产物)
eslint . --ext .ts,.tsx          # 执行lint检查
prettier --check .               # 格式检查
prettier --write .               # 自动修复格式问题
vitest                           # 运行测试(监听模式)
vitest run                       # 运行测试(CI模式)
vitest run --coverage            # 运行测试并生成覆盖率报告

ESLint Configuration (Flat Config)

ESLint配置(扁平化配置)

javascript
// eslint.config.mjs
import tseslint from "typescript-eslint";
import importPlugin from "eslint-plugin-import";

export default tseslint.config(
  ...tseslint.configs.strictTypeChecked,
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/explicit-function-return-type": ["warn", {
        allowExpressions: true,
      }],
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/strict-boolean-expressions": "error",
      "import/order": ["error", {
        groups: ["builtin", "external", "internal", "parent", "sibling"],
        "newlines-between": "always",
      }],
    },
  }
);
javascript
// eslint.config.mjs
import tseslint from "typescript-eslint";
import importPlugin from "eslint-plugin-import";

export default tseslint.config(
  ...tseslint.configs.strictTypeChecked,
  {
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/explicit-function-return-type": ["warn", {
        allowExpressions: true,
      }],
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/strict-boolean-expressions": "error",
      "import/order": ["error", {
        groups: ["builtin", "external", "internal", "parent", "sibling"],
        "newlines-between": "always",
      }],
    },
  }
);

Strict tsconfig.json

严格模式tsconfig.json配置

json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}
json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}

Input Validation (Zod)

输入校验(Zod)

typescript
import { z } from "zod";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150),
  role: z.enum(["admin", "user", "viewer"]),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

// At API boundary
function handleCreateUser(rawBody: unknown): CreateUserInput {
  return CreateUserSchema.parse(rawBody); // throws ZodError on failure
}
typescript
import { z } from "zod";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().min(0).max(150),
  role: z.enum(["admin", "user", "viewer"]),
});

type CreateUserInput = z.infer<typeof CreateUserSchema>;

// API边界使用
function handleCreateUser(rawBody: unknown): CreateUserInput {
  return CreateUserSchema.parse(rawBody); // 校验失败时抛出ZodError
}

Advanced Topics

进阶主题

For detailed patterns and examples, see:
  • references/patterns.md -- Advanced type patterns, Zod validation, async orchestration, module organization, React + TypeScript idioms
  • references/pitfalls.md -- Common TypeScript footguns, type narrowing mistakes, async traps
  • references/security.md -- XSS prevention, CSRF, content security policy, dependency auditing
如需了解详细模式和示例,可查看:
  • references/patterns.md -- 进阶类型模式、Zod校验、异步编排、模块组织、React + TypeScript惯用写法
  • references/pitfalls.md -- TypeScript常见踩坑点、类型收窄错误、异步陷阱
  • references/security.md -- XSS防护、CSRF、内容安全策略、依赖审计

External References

外部参考资料