深入理解 TypeScript 类型检查的重要性和如何用自动化工具保障项目质量。
目录
- [什么是 TypeScript 类型检查](#什么是 TypeScript 类型检查)
- 为什么它很重要
- 关键指标
- 脚本工作原理
- 使用方法
- 输出结果
- [CI/CD 集成](#CI/CD 集成)
- 常见问题
什么是 TypeScript 类型检查
核心概念
TypeScript 是 JavaScript 的超集,提供静态类型系统 。类型检查是在代码编译时检查数据类型是否正确使用。
typescript
// ❌ 错误:类型不匹配
const num: number = "hello";
str.toLowerCase2(); // 属性不存在
// ✅ 正确
const result: string = "test".toLowerCase();
检查的三个层面
- 语法检查 - 代码是否符合语法规则
- 类型检查 - 类型是否匹配和兼容
- 引用检查 - 变量、函数是否已定义
类型推导
即使没有显式类型注解,TypeScript 也能自动推导:
typescript
const x = 42; // 推导为 number
const name = "Alice"; // 推导为 string
x.toLowerCase(); // ❌ 错误:number 没有此方法
name.toLowerCase(); // ✅ 正确
为什么它很重要
🔴 尽早发现 Bug
JavaScript 中的错误只在用户使用时才会发现,成本高。
TypeScript 在开发时就能发现问题。
typescript
// TypeScript 开发时就会报错
function processUser(user: User): string {
return user.name.toUpperCase();
}
processUser(null); // ❌ 编译错误,避免了运行时崩溃
🚀 提升开发效率
类型信息让 IDE 提供精准的自动补全和智能提示:
typescript
const user = { name: "Alice", email: "alice@example.com" };
user. // ← IDE 自动列出所有属性和方法
🔒 重构的安全性
修改代码时,类型检查会立即发现所有的断裂点,无需手动排查:
typescript
// 修改 interface,所有使用不当的地方都会报错
interface Product {
id: number;
name: string;
category: string; // 新增字段
price: number;
}
// TypeScript 会列出所有需要修复的地方
👥 团队协作更顺畅
类型系统本质上是可执行的文档,代码意图一目了然:
typescript
// 有类型,一看就知道参数和返回值
function calculateTotal(items: Array<{price: number; quantity: number}>): number
// vs 没有类型
function calculateTotal(items) // 看不懂需要什么
关键指标
三个核心指标
1. 错误总数(Error Count)
项目中检测到的所有类型错误、警告的总数。
2. 代码行数(Total Lines)
项目源文件的总行数(排除 node_modules、.d.ts 等)。
3. 错误密度(Errors Per K Lines)✨ 最重要
错误密度 = (错误总数 × 1000) / 代码总行数
表示每 1000 行代码中平均有多少个错误
评分标准
| 错误密度 | 评级 | 说明 |
|---|---|---|
| 0 | ⭐⭐⭐⭐⭐ | 完美 |
| 0.1 - 0.5 | ⭐⭐⭐⭐ | 优秀 |
| 0.5 - 2.0 | ⭐⭐⭐ | 良好 |
| 2.0 - 5.0 | ⭐⭐ | 一般 |
| > 5.0 | ⭐ | 较差 |
实例对比
json
// 项目 A - 代码少但质量好
{
"totalLines": 5000,
"errorCount": 2,
"errorPerKLines": 0.4 // 优秀 ⭐⭐⭐⭐
}
// 项目 B - 代码多但问题也多
{
"totalLines": 5000,
"errorCount": 50,
"errorPerKLines": 10.0 // 较差 ⭐
}
错误分类
诊断信息分为三个级别:
- 🔴 Error - 严重错误,必须立即修复
- 🟡 Warning - 潜在问题,应该修复
- 🟢 Suggestion - 代码改进建议,可选
脚本工作原理
为什么用这个脚本?
| 方案 | 速度 | 灵活性 | 用途 |
|---|---|---|---|
tsc 命令行 |
⭐ 慢 | 差 | 一次性编译 |
| 编译器 API | ⭐⭐⭐⭐⭐ 快 | ⭐⭐⭐⭐⭐ | 自动化、CI/CD |
使用编译器 API 的优势:
- 🚀 性能快 5-10 倍(内存中处理,无磁盘 I/O)
- 🎯 灵活控制,易于集成
- 📊 结果可编程处理
执行流程
解析命令行参数
↓
检查目标目录存在
↓
查找 tsconfig.json ← 不存在则失败
↓
读取并解析配置 ← 格式错误则失败
↓
创建 TypeScript Program(加载所有源文件)
↓
执行类型检查(不生成 .js 文件)
↓
统计代码行数
↓
格式化诊断信息为 JSON
↓
计算关键指标
↓
输出结果文件
↓
设置退出码(成功 0,失败 1)
核心 API
javascript
const ts = require('typescript');
// 1. 加载配置
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, absTargetDir);
// 2. 创建编译程序
const program = ts.createProgram(parsed.fileNames, parsed.options);
// 3. 执行类型检查
const diagnostics = ts.getPreEmitDiagnostics(program);
// 4. 格式化结果
const result = diagnostics.map(d => ({
code: d.code,
category: ts.DiagnosticCategory[d.category],
message: ts.flattenDiagnosticMessageText(d.messageText, '\n'),
filePath: d.file.fileName,
line: d.file.getLineAndCharacterOfPosition(d.start).line + 1,
}));
使用方法
基本用法
bash
# 使用默认路径
node check-ts-errors.js
# 检查指定目录
node check-ts-errors.js /path/to/my-project
# 检查相对路径
node check-ts-errors.js ./src
查看结果
脚本会生成 ts-health-report-{projectName}.json 文件。
bash
# 查看完整结果
cat ts-health-report-my-project.json | jq '.'
# 只看关键指标
cat ts-health-report-my-project.json | jq '.checkInfo'
# 只看错误列表
cat ts-health-report-my-project.json | jq '.diagnostics[]'
输出结果
成功时的输出
json
{
"projectName": "my-project",
"checkTime": "2024-01-14T10:30:00.000Z",
"checkType": "tscheck",
"checkInfo": {
"totalLines": 15234,
"errorCount": 0,
"errorPerKLines": 0
},
"success": true,
"diagnostics": []
}
有错误时的输出
json
{
"projectName": "my-project",
"checkTime": "2024-01-14T10:30:00.000Z",
"checkInfo": {
"totalLines": 15234,
"errorCount": 3,
"errorPerKLines": 0.197
},
"success": false,
"diagnostics": [
{
"code": 2339,
"category": "Error",
"message": "Property 'toLowerCase' does not exist on type 'number'",
"file": "index.ts",
"filePath": "src/index.ts",
"line": 15,
"character": 8,
"lineText": "const result = num.toLowerCase();"
},
{
"code": 2322,
"category": "Error",
"message": "Type 'string' is not assignable to type 'number'",
"file": "utils.ts",
"filePath": "src/utils.ts",
"line": 42,
"character": 5,
"lineText": "const x: number = \"123\";"
}
]
}
诊断字段说明
| 字段 | 含义 |
|---|---|
code |
TypeScript 错误代码(2322、2339 等) |
category |
错误级别(Error、Warning、Suggestion) |
message |
错误描述 |
filePath |
文件相对路径 |
line |
行号(从 1 开始) |
character |
列号(从 1 开始) |
lineText |
源代码行 |
CI/CD 集成
GitHub Actions
在 .github/workflows/ts-check.yml:
yaml
name: TypeScript Check
on: [push, pull_request]
jobs:
ts-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- name: Check TypeScript
run: npm run check:ts > ts-report.json
continue-on-error: true
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('ts-report.json', 'utf8'));
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## TypeScript Health Check\n- Errors: ${report.checkInfo.errorCount}\n- Density: ${report.checkInfo.errorPerKLines}/K lines\n- Status: ${report.success ? '✅ PASS' : '❌ FAIL'}`
});
Git Hook(Pre-commit)
创建 .git/hooks/pre-commit:
bash
#!/bin/bash
echo "🔍 Checking TypeScript..."
npm run check:ts > /tmp/ts-check.json 2>&1
if [ $? -ne 0 ]; then
echo "❌ TypeScript check failed!"
cat /tmp/ts-check.json | jq '.diagnostics[] | " \(.filePath):\(.line) - \(.message)"'
exit 1
fi
echo "✅ TypeScript check passed"
exit 0
添加执行权限:
bash
chmod +x .git/hooks/pre-commit
数据分析
bash
# 按类型分组统计
cat ts-report.json | jq '.diagnostics | group_by(.category) | map({category: .[0].category, count: length})'
# 找出错误最多的文件(Top 5)
cat ts-report.json | jq '.diagnostics | group_by(.filePath) | map({file: .[0].filePath, count: length}) | sort_by(.count) | reverse | .[0:5]'
# 统计错误代码分布
cat ts-report.json | jq '.diagnostics | group_by(.code) | map({code: .[0].code, message: .[0].message, count: length}) | sort_by(.count) | reverse'
常见问题
Q:脚本执行很慢?
原因 :代码量大或 tsconfig.json 包含了不必要的目录。
解决:
优化 tsconfig.json:
json
{
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
增加 Node.js 内存:
bash
node --max-old-space-size=4096 check-ts-errors.js .
Q:IDE 和脚本检查结果不一致?
原因 :TypeScript 版本不同或 tsconfig.json 配置不同。
解决:
bash
# 检查版本
npm ls typescript
# 查看实际配置
node -e "const ts = require('typescript'); const config = ts.readConfigFile('./tsconfig.json', ts.sys.readFile); console.log(JSON.stringify(config.config, null, 2));"
Q:如何忽略某些错误?
使用 @ts-ignore 或 @ts-expect-error 注释:
typescript
// @ts-ignore
const x: string = 123;
或在 tsconfig.json 中配置:
json
{
"compilerOptions": {
"noImplicitAny": false,
"skipLibCheck": true
}
}
Q:常见的错误代码是什么?
| 代码 | 含义 | 示例 |
|---|---|---|
| 2322 | 类型不可分配 | const x: string = 123; |
| 2339 | 属性不存在 | "hello".toUpperCase2(); |
| 2345 | 参数类型不匹配 | const f = (x: string) => {}; f(123); |
| 2307 | 找不到模块 | import * from './nonexistent'; |
| 7006 | 参数隐式 any | 开启 noImplicitAny 时 |
完整列表参见 TypeScript 官方文档。
完整代码
#!/usr/bin/env node
/**
* 简单的 TS 错误检查脚本
*
* 用法:
* node check-ts-errors.js // 使用默认路径
* node check-ts-errors.js /your/path // 检查指定路径
*
* 脚本会在目标路径下查找 tsconfig.json,然后使用 TypeScript Compiler API
* 计算诊断信息,并以 JSON 格式输出所有 TS 错误。
*/
const fs = require('fs');
const path = require('path');
const ts = require('typescript');
// 默认要检查的项目路径,可以按需修改
const DEFAULT_TARGET_DIR = '';
function main() {
const targetDir = process.argv[2] || DEFAULT_TARGET_DIR;
const absTargetDir = path.isAbsolute(targetDir)
? targetDir
: path.resolve(process.cwd(), targetDir);
// 结果输出文件:默认写到当前执行目录下,文件名中带上目标目录名
const projectName = path.basename(absTargetDir);
const outputFile = path.resolve(
process.cwd(),
`ts-health-report-${projectName}.json`
);
const writeResultAndExit = (result, exitCode) => {
try {
fs.writeFileSync(outputFile, JSON.stringify(result, null, 2), 'utf8');
console.log(`已将检查结果写入文件:${outputFile}`);
} catch (e) {
console.error('写入结果文件失败,将结果输出到控制台:');
console.log(JSON.stringify(result, null, 2));
}
process.exit(exitCode);
};
console.log(`检查 TypeScript 错误,目标目录:${absTargetDir}`);
if (!fs.existsSync(absTargetDir)) {
console.error(`目标目录不存在:${absTargetDir}`);
process.exit(1);
}
const tsconfigPath = path.join(absTargetDir, 'tsconfig.json');
if (!fs.existsSync(tsconfigPath)) {
const now = new Date().toISOString();
const result = {
projectName,
checkTime: now,
checkType: 'tscheck',
checkInfo: {
totalLines: null,
errorCount: null,
errorPerKLines: null,
},
success: false,
error: {
type: 'NoTsconfig',
message: `未找到 tsconfig.json:${tsconfigPath}`,
},
diagnostics: [],
};
writeResultAndExit(result, 1);
}
// 读取并解析 tsconfig.json
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (configFile.error) {
const msg = ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n');
const now = new Date().toISOString();
const result = {
projectName,
checkTime: now,
checkType: 'tscheck',
checkInfo: {
totalLines: null,
errorCount: null,
errorPerKLines: null,
},
success: false,
error: {
type: 'TsconfigReadError',
message: msg,
},
diagnostics: [],
};
writeResultAndExit(result, 1);
}
const parsed = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
absTargetDir
);
const program = ts.createProgram(parsed.fileNames, parsed.options);
const diagnostics = ts.getPreEmitDiagnostics(program);
// 统计代码总行数(过滤掉 lib 文件和 node_modules)
let totalLines = 0;
const sourceFiles = program
.getSourceFiles()
.filter((sf) => {
const fileName = sf.fileName;
// 只统计目标目录内的文件,并排除 node_modules 和 TypeScript 自带库
if (!fileName.startsWith(absTargetDir)) return false;
if (fileName.includes('node_modules')) return false;
if (fileName.endsWith('.d.ts')) return false;
return true;
});
for (const sf of sourceFiles) {
try {
const text = fs.readFileSync(sf.fileName, 'utf8');
// 按行拆分,统计行数
const lines = text.split(/\r?\n/);
totalLines += lines.length;
} catch {
// 忽略读取失败的文件
}
}
const formattedDiagnostics = diagnostics.map((d) => {
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
const category = ts.DiagnosticCategory[d.category] || 'Unknown';
let file = null;
let filePath = null;
let line = null;
let character = null;
let lineText = null;
if (d.file && typeof d.start === 'number') {
const pos = d.file.getLineAndCharacterOfPosition(d.start);
file = d.file.fileName.split(path.sep).slice(-1)[0];
// 将绝对路径转换为相对于项目根目录的路径,方便跨环境对比
filePath = path.relative(absTargetDir, d.file.fileName) || '.';
line = pos.line + 1; // 1-based
character = pos.character + 1; // 1-based
try {
const text = d.file.text;
const lines = text.split(/\r?\n/);
const idx = pos.line;
if (idx >= 0 && idx < lines.length) {
// 去除该行前后的空白字符,避免前后多余缩进影响展示和统计
lineText = lines[idx].trim();
}
} catch {
lineText = null;
}
}
return {
code: d.code,
category,
message,
file,
filePath,
line,
character,
lineText,
};
});
const errorCount = formattedDiagnostics.length;
const errorPerKLines =
totalLines > 0 ? Number(((errorCount * 1000) / totalLines).toFixed(4)) : null;
const now = new Date().toISOString();
const result = {
projectName,
checkTime: now,
checkType: 'tscheck',
checkInfo: {
totalLines,
errorCount,
errorPerKLines,
},
success: errorCount === 0,
targetDir: absTargetDir,
tsconfigPath,
diagnostics: formattedDiagnostics,
};
// 有错误时返回非 0,方便在 CI 中使用
writeResultAndExit(result, result.success ? 0 : 1);
}
main();