Files
code-review-skill/reference/typescript.md
Tu Shaokun 755bd71381 feat: 大幅扩充代码审查指南内容
主要更新:
- typescript.md: 新增泛型、条件类型、映射类型、strict 模式、ESLint 规则 (~540 行)
- python.md: 新增类型注解、async/await、pytest、性能优化 (~1070 行)
- vue.md: 新增 Vue 3.5 特性 (defineModel, useTemplateRef, useId) (~920 行)
- rust.md: 新增取消安全性、spawn vs await 决策指南 (~840 行)
- react.md: 新增 useSuspenseQuery 限制说明和场景指南 (~870 行)
- README.md: 更新行数统计 (总计 6000+ 行)

新增内容约 2861 行代码审查指南和示例
2025-11-29 14:04:46 +08:00

13 KiB
Raw Permalink Blame History

TypeScript/JavaScript Code Review Guide

TypeScript 代码审查指南覆盖类型系统、泛型、条件类型、strict 模式、async/await 模式等核心主题。

目录


类型安全基础

避免使用 any

// ❌ Using any defeats type safety
function processData(data: any) {
  return data.value;  // 无类型检查,运行时可能崩溃
}

// ✅ Use proper types
interface DataPayload {
  value: string;
}
function processData(data: DataPayload) {
  return data.value;
}

// ✅ 未知类型用 unknown + 类型守卫
function processUnknown(data: unknown) {
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: string }).value;
  }
  throw new Error('Invalid data');
}

类型收窄

// ❌ 不安全的类型断言
function getLength(value: string | string[]) {
  return (value as string[]).length;  // 如果是 string 会出错
}

// ✅ 使用类型守卫
function getLength(value: string | string[]): number {
  if (Array.isArray(value)) {
    return value.length;
  }
  return value.length;
}

// ✅ 使用 in 操作符
interface Dog { bark(): void }
interface Cat { meow(): void }

function speak(animal: Dog | Cat) {
  if ('bark' in animal) {
    animal.bark();
  } else {
    animal.meow();
  }
}

字面量类型与 as const

// ❌ 类型过于宽泛
const config = {
  endpoint: '/api',
  method: 'GET'  // 类型是 string
};

// ✅ 使用 as const 获得字面量类型
const config = {
  endpoint: '/api',
  method: 'GET'
} as const;  // method 类型是 'GET'

// ✅ 用于函数参数
function request(method: 'GET' | 'POST', url: string) { ... }
request(config.method, config.endpoint);  // 正确!

泛型模式

基础泛型

// ❌ 重复代码
function getFirstString(arr: string[]): string | undefined {
  return arr[0];
}
function getFirstNumber(arr: number[]): number | undefined {
  return arr[0];
}

// ✅ 使用泛型
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

泛型约束

// ❌ 泛型没有约束,无法访问属性
function getProperty<T>(obj: T, key: string) {
  return obj[key];  // Error: 无法索引
}

// ✅ 使用 keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 30 };
getProperty(user, 'name');  // 返回类型是 string
getProperty(user, 'age');   // 返回类型是 number
getProperty(user, 'foo');   // Error: 'foo' 不在 keyof User

泛型默认值

// ✅ 提供合理的默认类型
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

// 可以不指定泛型参数
const response: ApiResponse = { data: null, status: 200, message: 'OK' };
// 也可以指定
const userResponse: ApiResponse<User> = { ... };

常见泛型工具类型

// ✅ 善用内置工具类型
interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;         // 所有属性可选
type RequiredUser = Required<User>;       // 所有属性必需
type ReadonlyUser = Readonly<User>;       // 所有属性只读
type UserKeys = keyof User;               // 'id' | 'name' | 'email'
type NameOnly = Pick<User, 'name'>;       // { name: string }
type WithoutId = Omit<User, 'id'>;        // { name: string; email: string }
type UserRecord = Record<string, User>;   // { [key: string]: User }

高级类型

条件类型

// ✅ 根据输入类型返回不同类型
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// ✅ 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never;

type Elem = ElementType<string[]>;  // string

// ✅ 提取函数返回类型(内置 ReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

映射类型

// ✅ 转换对象类型的所有属性
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  name: string;
  age: number;
}

type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }

// ✅ 添加前缀
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }

模板字面量类型

// ✅ 类型安全的事件名称
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'

// ✅ API 路由类型
type ApiRoute = `/api/${string}`;
const route: ApiRoute = '/api/users';  // OK
const badRoute: ApiRoute = '/users';   // Error

Discriminated Unions

// ✅ 使用判别属性实现类型安全
type Result<T, E> =
  | { success: true; data: T }
  | { success: false; error: E };

function handleResult(result: Result<User, Error>) {
  if (result.success) {
    console.log(result.data.name);  // TypeScript 知道 data 存在
  } else {
    console.log(result.error.message);  // TypeScript 知道 error 存在
  }
}

// ✅ Redux Action 模式
type Action =
  | { type: 'INCREMENT'; payload: number }
  | { type: 'DECREMENT'; payload: number }
  | { type: 'RESET' };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREMENT':
      return state + action.payload;  // payload 类型已知
    case 'DECREMENT':
      return state - action.payload;
    case 'RESET':
      return 0;  // 这里没有 payload
  }
}

Strict 模式配置

推荐的 tsconfig.json

{
  "compilerOptions": {
    // ✅ 必须开启的 strict 选项
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,

    // ✅ 额外推荐选项
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true
  }
}

noUncheckedIndexedAccess 的影响

// tsconfig: "noUncheckedIndexedAccess": true

const arr = [1, 2, 3];
const first = arr[0];  // 类型是 number | undefined

// ❌ 直接使用可能出错
console.log(first.toFixed(2));  // Error: 可能是 undefined

// ✅ 先检查
if (first !== undefined) {
  console.log(first.toFixed(2));
}

// ✅ 或使用非空断言(确定时)
console.log(arr[0]!.toFixed(2));

异步处理

Promise 错误处理

// ❌ Not handling async errors
async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();  // 网络错误未处理
}

// ✅ Handle errors properly
async function fetchUser(id: string): Promise<User> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`Failed to fetch user: ${error.message}`);
    }
    throw error;
  }
}

Promise.all vs Promise.allSettled

// ❌ Promise.all 一个失败全部失败
async function fetchAllUsers(ids: string[]) {
  const users = await Promise.all(ids.map(fetchUser));
  return users;  // 一个失败就全部失败
}

// ✅ Promise.allSettled 获取所有结果
async function fetchAllUsers(ids: string[]) {
  const results = await Promise.allSettled(ids.map(fetchUser));

  const users: User[] = [];
  const errors: Error[] = [];

  for (const result of results) {
    if (result.status === 'fulfilled') {
      users.push(result.value);
    } else {
      errors.push(result.reason);
    }
  }

  return { users, errors };
}

竞态条件处理

// ❌ 竞态条件:旧请求可能覆盖新请求
function useSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(setResults);  // 旧请求可能后返回!
  }, [query]);
}

// ✅ 使用 AbortController
function useSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const controller = new AbortController();

    fetch(`/api/search?q=${query}`, { signal: controller.signal })
      .then(r => r.json())
      .then(setResults)
      .catch(e => {
        if (e.name !== 'AbortError') throw e;
      });

    return () => controller.abort();
  }, [query]);
}

不可变性

Readonly 与 ReadonlyArray

// ❌ 可变参数可能被意外修改
function processUsers(users: User[]) {
  users.sort((a, b) => a.name.localeCompare(b.name));  // 修改了原数组!
  return users;
}

// ✅ 使用 readonly 防止修改
function processUsers(users: readonly User[]): User[] {
  return [...users].sort((a, b) => a.name.localeCompare(b.name));
}

// ✅ 深度只读
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

不变式函数参数

// ✅ 使用 as const 和 readonly 保护数据
function createConfig<T extends readonly string[]>(routes: T) {
  return routes;
}

const routes = createConfig(['home', 'about', 'contact'] as const);
// 类型是 readonly ['home', 'about', 'contact']

ESLint 规则

推荐的 @typescript-eslint 规则

// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:@typescript-eslint/strict'
  ],
  rules: {
    // ✅ 类型安全
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'error',
    '@typescript-eslint/no-unsafe-member-access': 'error',
    '@typescript-eslint/no-unsafe-call': 'error',
    '@typescript-eslint/no-unsafe-return': 'error',

    // ✅ 最佳实践
    '@typescript-eslint/explicit-function-return-type': 'warn',
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/await-thenable': 'error',
    '@typescript-eslint/no-misused-promises': 'error',

    // ✅ 代码风格
    '@typescript-eslint/consistent-type-imports': 'error',
    '@typescript-eslint/prefer-nullish-coalescing': 'error',
    '@typescript-eslint/prefer-optional-chain': 'error'
  }
};

常见 ESLint 错误修复

// ❌ no-floating-promises: Promise 必须被处理
async function save() { ... }
save();  // Error: 未处理的 Promise

// ✅ 显式处理
await save();
// 或
save().catch(console.error);
// 或明确忽略
void save();

// ❌ no-misused-promises: 不能在非 async 位置使用 Promise
const items = [1, 2, 3];
items.forEach(async (item) => {  // Error!
  await processItem(item);
});

// ✅ 使用 for...of
for (const item of items) {
  await processItem(item);
}
// 或 Promise.all
await Promise.all(items.map(processItem));

Review Checklist

类型系统

  • 没有使用 any(使用 unknown + 类型守卫代替)
  • 接口和类型定义完整且有意义的命名
  • 使用泛型提高代码复用性
  • 联合类型有正确的类型收窄
  • 善用工具类型Partial、Pick、Omit 等)

泛型

  • 泛型有适当的约束extends
  • 泛型参数有合理的默认值
  • 避免过度泛型化KISS 原则)

Strict 模式

  • tsconfig.json 启用了 strict: true
  • 启用了 noUncheckedIndexedAccess
  • 没有使用 @ts-ignore改用 @ts-expect-error

异步代码

  • async 函数有错误处理
  • Promise rejection 被正确处理
  • 没有 floating promises未处理的 Promise
  • 并发请求使用 Promise.all 或 Promise.allSettled
  • 竞态条件使用 AbortController 处理

不可变性

  • 不直接修改函数参数
  • 使用 spread 操作符创建新对象/数组
  • 考虑使用 readonly 修饰符

ESLint

  • 使用 @typescript-eslint/recommended
  • 没有 ESLint 警告或错误
  • 使用 consistent-type-imports