中大型 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 的主要来源以及类型断言的使用情况,避免为了提高百分比而掩盖问题。
备注:内容仅供参考。
