PAYATHON 2026

中大型 JavaScript/TypeScript 项目如何提升类型安全与开发体验?

数据阿明

建议建立一套稳定的检查流程:用严格配置的 TypeScript 做类型检查,用 typescript-eslint 补充需要类型信息的规则,将两者接入编辑器和 CI,再用 type-coverage 跟踪类型薄弱的区域。现有 JavaScript 模块可以从 // @ts-check 和 JSDoc 入手,逐步迁移。

typescript-eslint 可以发现编译器通常允许、但容易出错的写法。它使用 TypeScript 提供的类型信息,并不独立提供一套更强的类型推断系统。typescript-eslint 类型感知检查说明

先补齐 TypeScript 编译器检查

先确认项目在本地和 CI 中都运行了类型检查。IDE 自动补全正常,并不代表整个项目已经通过检查。

如果项目由其他工具负责输出 JavaScript,可以将下面的选项合并到现有 tsconfig.json 中,保留原有的 target、module、moduleResolution、jsx 等环境配置:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["src"]
}

各选项的作用:

  • strict:启用严格类型检查,包括隐式 any 和空值相关检查。
  • noUncheckedIndexedAccess:让数组或索引签名访问体现可能得到 undefined 的情况。
  • exactOptionalPropertyTypes:更严格地区分可选属性缺失和显式赋值为 undefined。
  • allowJs:允许 JavaScript 文件进入项目。
  • checkJs: false:暂不对所有 JavaScript 文件统一报告类型错误,方便按文件推进。
  • noEmit:只检查,不输出编译产物;如果项目依赖 tsc 构建,应按构建需求调整。

noUncheckedIndexedAccess 和 exactOptionalPropertyTypes 需要单独配置,开启 strict 并不会自动启用它们。TypeScript 配置参考

本地运行:

npx tsc --noEmit -p tsconfig.json

如果历史错误较多,可以先在边界清晰的模块或子包中启用严格检查,再逐步扩大范围。不过,缩小 include 不一定能隔离所有旧代码:被检查文件导入的依赖仍可能进入检查范围。

用 typescript-eslint 检查常见危险写法

可以优先采用支持类型信息的 recommendedTypeChecked 配置。值得关注的规则包括:

规则主要用途
no-explicit-any限制显式编写 any
no-unsafe-assignment发现来自 any 的不安全赋值
no-unsafe-call、no-unsafe-member-access发现对 any 的调用和属性访问
no-unsafe-argument、no-unsafe-return限制 any 在参数和返回值中传播
no-floating-promises发现缺少适当处理的 Promise

只禁止显式 any 还不够。第三方声明或现有 JavaScript 代码也可能让 any 流入业务代码,no-unsafe-* 系列规则可以帮助找出这些传播位置。不安全赋值规则说明、Promise 检查规则说明

安装所需的开发依赖,并选择彼此兼容的版本:

npm install --save-dev typescript eslint @eslint/js typescript-eslint

下面的示例适用于支持 ESLint flat config、defineConfig 和 projectService 的版本组合,可以作为现有 eslint.config.mjs 的增量配置:

import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig(
  {
    ignores: ['dist/**', 'coverage/**'],
  },
  {
    files: ['src/**/*.{ts,tsx,mts,cts}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommendedTypeChecked,
    ],
    languageOptions: {
      parserOptions: {
        projectService: true,
      },
    },
    rules: {
      '@typescript-eslint/no-explicit-any': 'error',
      '@typescript-eslint/no-floating-promises': 'error',
    },
  },
);

这里将类型感知检查限定在 TypeScript 源文件中,已有的 JavaScript 检查配置可以继续保留。projectService 根据文件所属的项目获取类型信息,受检查文件应包含在相应的 tsconfig.json 中。官方配置示例

迁移初期可以缩小规则的生效范围。等团队处理完主要问题后,再评估 strictTypeChecked,逐项确认规则是否适合项目。类型感知 lint 需要分析项目类型,执行成本通常高于普通语法规则。

JavaScript 模块如何渐进采用

对于暂时不准备改成 .ts 的文件,可以先启用单文件检查,并在函数边界补充 JSDoc:

// @ts-check

/**
 * @param {{ id: string, name: string }} user
 * @returns {string}
 */
export function formatUser(user) {
  return `${user.id}: ${user.name}`;
}

在前面的配置中,allowJs: true 让文件进入项目,// @ts-check 为选中的文件启用检查。等大部分 JavaScript 文件具备可用的类型信息后,再考虑开启全局 checkJs: true。JavaScript 检查选项说明

迁移时可以优先处理公共工具函数、数据结构和被频繁调用的模块。明确这些边界后,下游代码才能获得更可靠的推断。缺少第三方类型声明时,应补充准确的声明,避免直接将整个模块声明为 any。

如何兼顾类型推断和开发体验

局部变量尽量使用推断,在导出函数、公共接口和复杂数据边界补充必要的类型。给每个中间变量重复标注类型,往往只会增加维护量。

如果使用 TypeScript 4.9 或更高版本,可以用 satisfies 检查配置对象是否符合约束,同时保留有用的属性推断:

type ServiceConfig = {
  mode: 'development' | 'production';
  retries: number;
};

const config = {
  mode: 'production',
  retries: 3,
} satisfies ServiceConfig;

satisfies 适合配置表等场景,但不负责验证运行时数据。TypeScript satisfies 说明

对于接口响应、用户输入和存储数据,应先把不可信值视为 unknown,经过实际检查后再使用。类型断言 as SomeType 不会验证数据,也不应作为消除错误的常规手段。

编辑器、命令行和 CI 应使用一致的 TypeScript 版本及项目配置。如果编辑器和 lint 给出不同结果,先检查版本、文件归属和编译选项是否一致。类型感知检查排查说明

如何接入 CI/CD 并支撑更大的项目

可以在 package.json 中增加独立检查命令:

{
  "scripts": {
    "typecheck": "tsc --noEmit -p tsconfig.json",
    "lint": "eslint src --max-warnings 0"
  }
}

对于使用 npm 且已提交锁文件的项目,CI 可以执行:

npm ci
npm run typecheck
npm run lint

将这些命令设为合并前必须通过的检查。提交前检查可以提供快速反馈,CI 仍应覆盖完整项目,或使用能够正确追踪依赖影响范围的检查流程。只检查本次修改的文件,可能漏掉公共类型变更对调用方的影响。

如果代码库已经按包组织,并出现明显的检查或编辑器性能问题,可以评估 Project References。它支持将程序拆成多个相互引用的项目,配合 tsc -b 按依赖关系构建。采用前需要设计 composite、声明文件输出和包依赖关系,不能直接照搬单项目的 noEmit 配置。Project References 官方说明

如何分析和呈现类型覆盖率

可以从 type-coverage 开始。它的基础指标是非 any 类型标识符占全部统计标识符的比例,适合用来定位类型薄弱的区域。

npm install --save-dev type-coverage
npx type-coverage -p tsconfig.json --detail

如果当前基线已经达到目标,可以添加阈值检查。下面的 95 仅是示例,应根据项目实际情况设定:

npx type-coverage -p tsconfig.json --at-least 95

--detail 用于查看具体位置。需要接入报表或趋势图时,可以使用工具支持的 --json-output,将结果保存为 CI 产物,再按模块、提交或时间展示。固定工具版本、检查范围和统计选项,数据才具有可比性。type-coverage 使用说明

覆盖率可以用来追踪进展,但不能证明类型声明正确,也不能替代运行时校验和测试。建议同时关注未检查的 JavaScript 文件、any 的主要来源以及类型断言的使用情况,避免为了提高百分比而掩盖问题。

备注:内容仅供参考。