Skip to content

Latest commit

 

History

History
835 lines (661 loc) · 20.6 KB

01-code-quality.md

File metadata and controls

835 lines (661 loc) · 20.6 KB

代码质量工具

第七部分 - 第 1 节 | 面向 Java 工程师的 TypeScript 学习指南


概览

在 Java 世界中,你可能已经熟悉 Checkstyle、PMD、SpotBugs 等代码质量工具,以及 Maven enforcer 来强制执行规范。TypeScript/JavaScript 生态有一套对应的成熟工具链:

用途 Java 工具 TypeScript/JS 工具
代码规范检查 Checkstyle / PMD ESLint
代码格式化 google-java-format / Spotless Prettier
类型检查 javac 本身 tsc(TypeScript 编译器)
Git 提交钩子 Maven enforcer / Git hooks husky + lint-staged
提交信息规范 无标准工具 commitlint

1. ESLint 详解

1.1 ESLint 是什么

ESLint 是 JavaScript/TypeScript 生态最主流的静态代码分析工具。它可以:

  • 发现代码中的问题(类似 PMD/SpotBugs)
  • 强制代码风格规范(类似 Checkstyle)
  • 自动修复部分问题

Java 对比:Checkstyle 侧重代码格式,PMD 侧重代码质量,而 ESLint 把两者合并到一个工具中。

1.2 安装

# 初始化项目(如果还没有)
npm init -y

# 安装 ESLint 和 TypeScript 支持
npm install -D eslint @eslint/js typescript-eslint typescript

1.3 Flat Config 配置(eslint.config.js)

ESLint v9+ 使用全新的 Flat Config 格式,配置文件为 eslint.config.js(不再是 .eslintrc)。

注意:如果你搜索到的教程还在用 .eslintrc.json,那是旧版配置格式。新项目请统一使用 Flat Config。

// eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  // 1. ESLint 内置推荐规则
  eslint.configs.recommended,

  // 2. TypeScript ESLint 推荐规则
  ...tseslint.configs.recommended,

  // 3. 自定义配置
  {
    // 指定适用的文件
    files: ['src/**/*.ts'],

    languageOptions: {
      parserOptions: {
        // 启用类型感知的规则(需要 tsconfig)
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },

    rules: {
      // ===== 关闭不需要的规则 =====
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',        // 允许 _xxx 形式的未使用参数
        varsIgnorePattern: '^_',
      }],

      // ===== 类型安全规则 =====
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-non-null-assertion': 'warn',

      // ===== 最佳实践 =====
      '@typescript-eslint/prefer-nullish-coalescing': 'error',
      '@typescript-eslint/prefer-optional-chain': 'error',
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/await-thenable': 'error',

      // ===== 代码风格 =====
      '@typescript-eslint/consistent-type-imports': ['error', {
        prefer: 'type-imports',
      }],
      '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
    },
  },

  // 4. 忽略特定目录
  {
    ignores: ['dist/', 'node_modules/', '*.js'],
  },
);

Java 工程师理解方式:

  • eslint.configs.recommended 类似 Checkstyle 的 google_checks.xml 预设
  • tseslint.configs.recommended 类似为 Java 17+ 特性增加的额外规则
  • rules 部分类似在 checkstyle.xml 中自定义每条规则的 severity

1.4 TypeScript ESLint 插件

TypeScript ESLint 提供了三个级别的规则集:

// eslint.config.js
import tseslint from 'typescript-eslint';

export default tseslint.config(
  // 级别 1:基本推荐(不需要类型信息)
  ...tseslint.configs.recommended,

  // 级别 2:严格模式(不需要类型信息,更严格)
  // ...tseslint.configs.strict,

  // 级别 3:需要类型信息的规则(最完整,但需要 tsconfig)
  // ...tseslint.configs.recommendedTypeChecked,
  // ...tseslint.configs.strictTypeChecked,
);

常用 TypeScript ESLint 规则说明:

规则 说明 Java 对比
no-explicit-any 禁止使用 any 类型 类似禁止 raw type
no-floating-promises Promise 必须被处理 类似 checked exception
no-misused-promises 防止 Promise 误用 无直接对比
strict-boolean-expressions 条件表达式必须是 boolean Java 本身就要求
no-unnecessary-condition 检查永远为真/假的条件 类似 SpotBugs 的死代码检查
prefer-nullish-coalescing 使用 ?? 替代 || 无对比(JS 特有问题)

1.5 运行 ESLint

# 检查所有 TypeScript 文件
npx eslint src/

# 自动修复可以修复的问题
npx eslint src/ --fix

# 只检查特定文件
npx eslint src/index.ts

在 package.json 中添加脚本:

{
  "scripts": {
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix"
  }
}

1.6 自定义规则

ESLint 支持编写自定义规则(类似 PMD 自定义规则)。大多数情况下你不需要自定义,但了解原理有帮助:

// eslint-plugin-custom/no-console-log.js
// 自定义规则:禁止 console.log(允许 console.warn/error)
export default {
  meta: {
    type: 'suggestion',
    docs: {
      description: '禁止使用 console.log',
    },
    fixable: 'code',
    schema: [],
  },
  create(context) {
    return {
      // 访问 AST 中的 MemberExpression 节点
      MemberExpression(node) {
        if (
          node.object.type === 'Identifier' &&
          node.object.name === 'console' &&
          node.property.type === 'Identifier' &&
          node.property.name === 'log'
        ) {
          context.report({
            node,
            message: '不允许使用 console.log,请使用 logger 工具',
          });
        }
      },
    };
  },
};

在配置中使用自定义规则:

// eslint.config.js
import noConsoleLog from './eslint-plugin-custom/no-console-log.js';

export default [
  {
    plugins: {
      custom: {
        rules: {
          'no-console-log': noConsoleLog,
        },
      },
    },
    rules: {
      'custom/no-console-log': 'error',
    },
  },
];

2. Prettier 格式化

2.1 Prettier 是什么

Prettier 是一个有主见的(opinionated)代码格式化工具。它不关心代码逻辑,只关心代码长什么样。

Java 对比:类似 google-java-format——你几乎不需要配置,它帮你统一代码风格。

ESLint vs Prettier 的分工:

  • ESLint:代码质量(发现 bug、最佳实践)+ 少量格式规则
  • Prettier:纯粹的代码格式化(缩进、换行、引号等)

2.2 安装和配置

npm install -D prettier

创建配置文件 .prettierrc:

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

配置项说明:

选项 值 说明
semi true 行尾加分号(Java 工程师习惯)
singleQuote true 使用单引号(JS/TS 社区惯例)
tabWidth 2 缩进 2 空格(JS 惯例,Java 通常 4 空格)
trailingComma "all" 尾随逗号(便于 git diff)
printWidth 100 行宽 100 字符
arrowParens "always" 箭头函数始终加括号 (x) => x

创建忽略文件 .prettierignore:

dist/
node_modules/
*.md

运行 Prettier:

# 检查格式
npx prettier --check src/

# 自动格式化
npx prettier --write src/

在 package.json 中添加脚本:

{
  "scripts": {
    "format": "prettier --write src/",
    "format:check": "prettier --check src/"
  }
}

2.3 ESLint + Prettier 配合(避免冲突)

ESLint 有一些格式相关的规则(比如缩进、引号),会和 Prettier 冲突。解决方案是安装 eslint-config-prettier 来关闭 ESLint 中所有与 Prettier 冲突的规则:

npm install -D eslint-config-prettier
// eslint.config.js
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  // 放在最后,关闭冲突的格式规则
  eslintConfigPrettier,
  {
    files: ['src/**/*.ts'],
    rules: {
      // 你的自定义规则...
    },
  },
);

工作流:

  1. ESLint 负责代码质量检查
  2. Prettier 负责代码格式化
  3. eslint-config-prettier 消除两者的规则冲突

3. TypeScript 严格模式最佳配置

3.1 strict 模式各选项详解

TypeScript 的 strict: true 实际上是一组严格检查选项的集合:

{
  "compilerOptions": {
    "strict": true
    // 等价于同时开启以下所有选项:
    // "strictNullChecks": true,         // null/undefined 必须显式处理
    // "strictFunctionTypes": true,      // 函数参数类型逆变检查
    // "strictBindCallApply": true,      // bind/call/apply 类型检查
    // "strictPropertyInitialization": true, // 类属性必须初始化
    // "noImplicitAny": true,            // 禁止隐式 any
    // "noImplicitThis": true,           // 禁止隐式 this 类型
    // "alwaysStrict": true,             // 生成 "use strict"
    // "useUnknownInCatchVariables": true // catch 变量为 unknown
  }
}

Java 工程师理解:

选项 Java 对比
strictNullChecks 类似 Java 的 @NonNull / @Nullable 注解,但编译器强制检查
strictPropertyInitialization 类似 Java 中 final 字段必须在构造函数中初始化
noImplicitAny 类似禁止使用 raw type(List 而非 List<String>)
useUnknownInCatchVariables 类似 Java 中 catch 不能直接当特定异常处理

3.2 进阶选项

除了 strict 包含的选项外,还有一些推荐开启的进阶选项:

{
  "compilerOptions": {
    // strict 之外的推荐选项

    // 访问数组/对象索引时,类型包含 undefined
    // 例如:arr[0] 的类型是 T | undefined,而非 T
    "noUncheckedIndexedAccess": true,

    // 未使用的局部变量报错
    "noUnusedLocals": true,

    // 未使用的函数参数报错
    "noUnusedParameters": true,

    // switch 语句必须有 default 或者覆盖所有 case
    "noFallthroughCasesInSwitch": true,

    // 确保 import 类型时使用 type 关键字
    "verbatimModuleSyntax": true,

    // 强制使用 .js 扩展名(ESM 规范)
    "moduleResolution": "NodeNext",
    "module": "NodeNext"
  }
}

重点:noUncheckedIndexedAccess

这是一个非常实用但常被忽略的选项:

const arr = [1, 2, 3];

// 没有 noUncheckedIndexedAccess:
const first = arr[0]; // 类型是 number —— 但如果数组为空呢?

// 开启 noUncheckedIndexedAccess:
const first = arr[0]; // 类型是 number | undefined —— 更安全!

// 使用时必须检查
if (first !== undefined) {
  console.log(first.toFixed(2)); // 这里 first 是 number
}

// 或者用非空断言(确信不为空时)
const definitelyFirst = arr[0]!; // 类型是 number

Java 对比:Java 中 array[0] 如果越界会抛出 ArrayIndexOutOfBoundsException。TypeScript 开启此选项后,在编译时就提醒你处理可能的 undefined。

3.3 推荐的完整 tsconfig.json

{
  "compilerOptions": {
    // ===== 类型检查(最严格)=====
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,

    // ===== 编译目标 =====
    "target": "ES2022",
    "lib": ["ES2022"],

    // ===== 模块系统 =====
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "verbatimModuleSyntax": true,

    // ===== 输出 =====
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // ===== 互操作性 =====
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,

    // ===== 其他 =====
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4. Git Hooks

4.1 概念

Git Hooks 允许在 Git 操作的特定时机执行自定义脚本。最常用的场景:

  • pre-commit:提交前检查代码质量和格式
  • commit-msg:检查提交信息是否符合规范

Java 对比:类似 Maven enforcer 插件在 validate 阶段检查,或者 CI 中的 quality gate,但更早——在本地提交时就检查。

4.2 husky 安装配置

husky 是管理 Git Hooks 的工具:

# 安装
npm install -D husky

# 初始化(创建 .husky/ 目录)
npx husky init

这会:

  1. 创建 .husky/ 目录
  2. 在 package.json 中添加 prepare 脚本
  3. 创建一个示例 pre-commit hook

编辑 .husky/pre-commit:

# .husky/pre-commit
npx lint-staged

编辑 .husky/commit-msg:

# .husky/commit-msg
npx --no -- commitlint --edit $1

4.3 lint-staged:只检查变更文件

lint-staged 只对 Git 暂存区(staged) 的文件执行检查,而非全部文件。这样大型项目也能快速完成检查。

Java 对比:类似只对你改动的文件执行 Checkstyle,而非整个项目。

npm install -D lint-staged

在 package.json 中配置:

{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yml,yaml}": [
      "prettier --write"
    ]
  }
}

工作流:

git add src/index.ts
git commit -m "feat: add user login"
  └── 触发 pre-commit hook
       └── lint-staged 执行
            ├── eslint --fix src/index.ts
            └── prettier --write src/index.ts
            └── 如果检查通过,提交成功
            └── 如果检查失败,提交被阻止

4.4 commitlint:提交信息规范

commitlint 强制提交信息遵循 Conventional Commits 格式:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

常见 type:

type 说明 示例
feat 新功能 feat(auth): add JWT login
fix 修复 bug fix(api): handle null response
docs 文档变更 docs: update README
style 格式调整(不影响逻辑) style: fix indentation
refactor 重构 refactor(user): extract validation
test 测试 test: add login unit tests
chore 构建/工具变更 chore: update eslint config

安装和配置:

npm install -D @commitlint/cli @commitlint/config-conventional

创建 commitlint.config.js:

// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    // type 必须是以下之一
    'type-enum': [
      2, // 错误级别:0=关闭, 1=警告, 2=错误
      'always',
      [
        'feat', 'fix', 'docs', 'style', 'refactor',
        'test', 'chore', 'perf', 'ci', 'build', 'revert',
      ],
    ],
    // 主题不超过 72 个字符
    'subject-max-length': [2, 'always', 72],
  },
};

验证:

# 合法的提交信息
echo "feat(auth): add JWT authentication" | npx commitlint

# 不合法的提交信息(会报错)
echo "added some stuff" | npx commitlint
# => ✖   subject may not be empty [subject-empty]
# => ✖   type may not be empty [type-empty]

5. 完整配置示例

下面是一个完整的 TypeScript 项目的代码质量工具配置。

5.1 package.json

{
  "name": "my-ts-project",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/",
    "format:check": "prettier --check src/",
    "check": "tsc --noEmit && eslint src/ && prettier --check src/",
    "prepare": "husky"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.0.0",
    "@commitlint/config-conventional": "^19.0.0",
    "@eslint/js": "^9.0.0",
    "eslint": "^9.0.0",
    "eslint-config-prettier": "^9.0.0",
    "husky": "^9.0.0",
    "lint-staged": "^15.0.0",
    "prettier": "^3.0.0",
    "typescript": "^5.5.0",
    "typescript-eslint": "^8.0.0"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yml,yaml}": [
      "prettier --write"
    ]
  }
}

5.2 eslint.config.js

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  eslintConfigPrettier,
  {
    files: ['src/**/*.ts'],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', {
        argsIgnorePattern: '^_',
        varsIgnorePattern: '^_',
      }],
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/prefer-nullish-coalescing': 'error',
      '@typescript-eslint/prefer-optional-chain': 'error',
      '@typescript-eslint/consistent-type-imports': ['error', {
        prefer: 'type-imports',
      }],
    },
  },
  {
    ignores: ['dist/', 'node_modules/'],
  },
);

5.3 .prettierrc

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

5.4 tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "verbatimModuleSyntax": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

5.5 commitlint.config.js

export default {
  extends: ['@commitlint/config-conventional'],
};

5.6 初始化命令

# 完整的项目初始化流程
npm init -y
npm install -D typescript @eslint/js eslint typescript-eslint \
  eslint-config-prettier prettier \
  husky lint-staged \
  @commitlint/cli @commitlint/config-conventional

# 初始化 TypeScript
npx tsc --init

# 初始化 husky
npx husky init

# 创建 pre-commit hook
echo "npx lint-staged" > .husky/pre-commit

# 创建 commit-msg hook
echo 'npx --no -- commitlint --edit $1' > .husky/commit-msg

6. VS Code 集成

在团队中,推荐添加 VS Code 工作区设置,让编辑器自动应用格式化:

.vscode/settings.json

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

.vscode/extensions.json

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "ms-vscode.vscode-typescript-next"
  ]
}

小结

工具 职责 运行时机
TypeScript (tsc) 类型检查 开发时 / CI
ESLint 代码质量 + 最佳实践 开发时 / pre-commit / CI
Prettier 代码格式化 保存时 / pre-commit / CI
husky 管理 Git Hooks git commit 时
lint-staged 只检查暂存文件 pre-commit 时
commitlint 提交信息规范 commit-msg 时

推荐的 CI 检查命令:

# 在 CI 中运行所有检查
tsc --noEmit           # 类型检查(不生成文件)
eslint src/            # 代码质量检查
prettier --check src/  # 格式检查

下一节:我们将学习如何为 TypeScript 项目编写测试,使用 Vitest 框架进行单元测试、集成测试和 Mock。