TypeScript 错误类型检查,前端ts错误指南

深入理解 TypeScript 类型检查的重要性和如何用自动化工具保障项目质量。

目录

  1. [什么是 TypeScript 类型检查](#什么是 TypeScript 类型检查)
  2. 为什么它很重要
  3. 关键指标
  4. 脚本工作原理
  5. 使用方法
  6. 输出结果
  7. [CI/CD 集成](#CI/CD 集成)
  8. 常见问题

什么是 TypeScript 类型检查

核心概念

TypeScript 是 JavaScript 的超集,提供静态类型系统 。类型检查是在代码编译时检查数据类型是否正确使用。

typescript 复制代码
// ❌ 错误:类型不匹配
const num: number = "hello";
str.toLowerCase2();  // 属性不存在

// ✅ 正确
const result: string = "test".toLowerCase();

检查的三个层面

  1. 语法检查 - 代码是否符合语法规则
  2. 类型检查 - 类型是否匹配和兼容
  3. 引用检查 - 变量、函数是否已定义

类型推导

即使没有显式类型注解,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();
相关推荐
纆兰2 小时前
汇款单的完成
前端·javascript·html
酷酷的鱼2 小时前
2026 React Native新架构核心:JSI底层原理与老架构深度对比
react native·react.js·架构
Lsx_2 小时前
案例+图解带你遨游 Canvas 2D绘图 Fabric.js🔥🔥(5W+字)
前端·javascript·canvas
lili-felicity2 小时前
React Native 鸿蒙跨平台开发:动态表单全场景实现
react native·harmonyos
2501_944521002 小时前
rn_for_openharmony商城项目app实战-主题设置实现
javascript·数据库·react native·react.js·ecmascript
m0_471199633 小时前
【场景】如何快速接手一个前端项目
前端·vue.js·react.js
编程之路从0到13 小时前
ReactNative新架构之Android端TurboModule机制完全解析
android·react native·源码阅读
榴莲CC3 小时前
抗干扰LED数显屏驱动VK1624 数码管显示芯片 3线串行接口
前端
lili-felicity3 小时前
React Native for Harmony 个人消息列表最新消息置顶实现(多维度权重统计)
javascript·react native·react.js