typescript-guide
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTypeScript/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
核心原则
- Strict TypeScript: Enable all strict flags; treat type errors as build failures
- Immutability by Default: Use ,
const,readonly, and spread operators; mutate only when profiling demands itas const - Explicit Types at Boundaries: All function signatures, API responses, and public interfaces must have explicit type annotations; infer internally
- Functional Patterns: Prefer pure functions, map/filter/reduce, and composition over classes and mutation
- Zero : Use
anyfor truly unknown data, Zod/io-ts for runtime narrowing; everyunknownrequires a code-review comment explaining whyany
- 严格TypeScript配置:开启所有严格模式标识,将类型错误视为构建失败
- 默认不可变:使用、
const、readonly和展开运算符,仅当性能分析明确要求时才执行可变操作as const - 边界显式类型:所有函数签名、API响应和公共接口必须有显式类型注解,内部逻辑可使用类型推断
- 函数式编程模式:优先使用纯函数、map/filter/reduce和组合式写法,而非类和可变操作
- 零使用:对于完全未知的数据使用
any,使用Zod/io-ts做运行时类型收窄,每一处unknown都需要在代码评审注释中说明原因any
Guardrails
规范约束
TypeScript Configuration
TypeScript配置
- Enable (this activates
"strict": true,strictNullChecks,noImplicitAny, etc.)strictFunctionTypes - Enable (arrays and records return
"noUncheckedIndexedAccess": true)T | undefined - Enable and
"noImplicitReturns": true"noFallthroughCasesInSwitch": true - Set ,
"target": "ES2022","module": "ESNext""moduleResolution": "bundler" - Never suppress errors with ; use
@ts-ignorewith a justification comment@ts-expect-error
- 开启(会自动启用
"strict": true、strictNullChecks、noImplicitAny等规则)strictFunctionTypes - 开启(数组和record返回类型为
"noUncheckedIndexedAccess": true)T | undefined - 开启和
"noImplicitReturns": true"noFallthroughCasesInSwitch": true - 设置、
"target": "ES2022"、"module": "ESNext""moduleResolution": "bundler" - 禁止使用屏蔽错误,需使用
@ts-ignore并附带说明注释@ts-expect-error
Code Style
代码风格
- over
const; never useletvar - 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 () only at package boundaries, not within internal modules (causes circular imports and tree-shaking failures)
index.ts - Enums: prefer objects over TypeScript
as const(enums produce runtime code and have surprising behaviors with reverse mappings)enum
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,禁止使用letvar - 空值合并运算符()优先于逻辑或(
??)设置默认值(避免||、0、""这类假值引发的bug)false - 可选链()优先于多层嵌套空值检查
?. - 模板字面量优先于字符串拼接
- 调用侧使用解构:
const { id, name } = user - 仅在包边界使用桶导出(),内部模块禁止使用(会导致循环依赖和tree-shaking失效)
index.ts - 枚举:优先使用对象而非TypeScript原生
as const(枚举会生成运行时代码,且反向映射存在不符合预期的行为)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 block must type-narrow the error before accessing properties
catch - Never throw primitive values (strings, numbers); always throw subclasses
Error - Use the option for error chaining:
causenew AppError("msg", { cause: original }) - Never swallow errors with empty blocks
catch - Async functions must have error handling at the call site or a global boundary handler
- 每个块在访问属性前必须对错误做类型收窄
catch - 禁止抛出原始值(字符串、数字),始终抛出子类
Error - 使用参数做错误链传递:
causenew AppError("msg", { cause: original }) - 禁止使用空块吞掉错误
catch - 异步函数必须在调用侧处理错误,或在全局边界统一处理
Async/Await
Async/Await
- Always or return promises; never create fire-and-forget promises without explicit
awaitannotationvoid - Use for independent concurrent operations, not sequential
Promise.allin a loopawait - Set timeouts on all external calls (fetch, database, third-party APIs) using
AbortController - Use when partial failure is acceptable
Promise.allSettled - Never mix chains with
.then()in the same functionawait
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执行或返回,禁止创建即发即弃的promise,除非显式标注
awaitvoid - 独立的并发操作使用,而非循环中顺序
Promise.allawait - 所有外部调用(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) exclusively; noexportin TypeScriptrequire() - One export per concept: prefer named exports over default exports (improves refactoring, grep-ability)
- Side-effect imports () must be documented with a comment explaining why
import "./setup" - Re-export from only at package/feature boundaries
index.ts - Keep import order: stdlib/node builtins, external packages, internal modules, relative paths (enforce with ESLint )
import/order
- 仅使用ES模块(/
import),TypeScript中禁止使用exportrequire() - 每个概念单独导出:优先使用命名导出而非默认导出(提升重构效率和可检索性)
- 副作用导入()必须附带注释说明原因
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 must have zero I/O dependencies (pure functions, easy to test)
domain/ - No business logic in route handlers; delegate to services
- Shared types in ; co-locate component-specific types with their module
types/
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- 下的领域逻辑必须无I/O依赖(纯函数,易于测试)
domain/ - 路由处理器中不编写业务逻辑,统一委托给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: numbertypescript
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); // 类型安全:numberTyped 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 or under
*.test.tsmirroringtests/src/ - Naming: with
describe("ModuleName")it("should [expected behavior] when [condition]") - Use for setup; avoid
beforeEachfor mutable statebeforeAll - Coverage target: >80% for business logic, >60% overall
- No in test files; test helpers must be typed
any - Mock at boundaries (HTTP, database, file system), not internal functions
- 测试运行器:优先使用Vitest,也可使用Jest
- 测试文件:可与业务代码放在一起命名为,或放在
*.test.ts目录下与tests/结构对齐src/ - 命名规范:,测试用例命名为
describe("模块名")it("should [预期行为] when [触发条件]") - 使用做测试准备,可变状态场景避免使用
beforeEachbeforeAll - 覆盖率目标:业务逻辑覆盖率>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 coveragebash
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、内容安全策略、依赖审计