前端开发··2 阅读·预计 14 分钟

将 ChatGPT 嵌入 TypeScript 编译管线:用 CLI 工具自动化生成类型声明与单元测试

一、痛点:类型声明与单测的"重复劳动"陷阱

接手一个 200+ 接口的后端项目时,前端同学通常会面对这种场景:

// 这是后端给的 Swagger 字段,你要手写类型
// GET /api/users/:id
{
  "id": 1,
  "name": "Alice",
  "roles": ["admin", "editor"],
  "metadata": { "lastLogin": "2026-01-01", "device": "mac" }
}

然后就是无休止的"翻译工作":

// ❌ 纯手工,一百个接口写一天
interface UserResponse {
  id: number
  name: string
  roles: string[]
  metadata: {
    lastLogin: string
    device: string
  }
}

更糟的是单元测试——要给每个接口写 happy-path、空数组、缺失字段等 case,重复度极高。

本文的方案:用 ChatGPT 承担这个"翻译层",通过工程化校验确保输出可靠。


二、整体架构:三阶段管线

Swagger/接口文档
      │
      ▼
┌──────────────────┐
│  1. 类型提取器    │ → TypeScript Compiler API
└──────────────────┘
      │
      ▼
┌──────────────────┐
│  2. ChatGPT 生成  │ → 结构化 Prompt + JSON Mode
└──────────────────┘
      │
      ▼
┌──────────────────┐
│  3. 确定性校验    │ → tsc --noEmit + AST 一致性检查
└──────────────────┘

第一层和第三层是确定性的,AI 只在第二层作为"模式识别+代码生成"引擎。


三、阶段一:用 Compiler API 提取类型签名

不解析 JSON,而是先让 tsc 编译一遍已有的类型文件,再用 Compiler API 提取 AST:

import ts from 'typescript'

function extractInterfaces(filePath: string): Map<string, InterfaceDef> {
  const program = ts.createProgram([filePath], {})
  const sourceFile = program.getSourceFile(filePath)!
  const interfaces = new Map<string, InterfaceDef>()

  function visit(node: ts.Node) {
    if (ts.isInterfaceDeclaration(node)) {
      const name = node.name.text
      const members = node.members.map(m => {
        if (ts.isPropertySignature(m)) {
          return {
            name: (m.name as ts.Identifier).text,
            type: m.type?.getText(sourceFile) ?? 'any',
            optional: !!m.questionToken
          }
        }
      }).filter(Boolean) as PropertyDef[]
      interfaces.set(name, { name, members })
    }
    ts.forEachChild(node, visit)
  }
  visit(sourceFile)
  return interfaces
}

此时得到的是精确的类型元数据,不是模糊的自然语言。这作为 Prompt 的结构化输入喂给 ChatGPT,避免它猜测类型。


四、阶段二:结构化 Prompt 设计

关键原则:限制输出格式,不限制推理过程

function buildPrompt(interfaceName: string, members: PropertyDef[]): string {
  const signature = members
    .map(m => `${m.name}${m.optional ? '?' : ''}: ${m.type}`)
    .join('\n')

  return `
你是一个 TypeScript 测试代码生成器。根据以下接口签名,生成单元测试。

接口名: ${interfaceName}
字段签名:
${signature}

生成规则:
1. 每个接口生成 3 个 test case:合法数据、空值处理、边界值
2. 使用 vitest 的 describe/it/expect 语法
3. 只输出测试代码,不输出解释
4. 输出的代码必须可以独立通过 tsc --noEmit 检查

输出格式:直接输出 TypeScript 代码块。
`.trim()
}

❌ 错误示范:Prompt 过于模糊

// Bad
const badPrompt = `请为 ${interfaceName} 写一些测试`
// → ChatGPT 可能返回 RN 测试代码、Jest 语法、甚至 Markdown 说明

✅ 正确示范:约束输出格式和代码规范

约束"只输出代码"后,解析层就只需做一件事:把返回内容写进 .test.ts 文件。


五、ChatGPT API 调用层:流式处理 + JSON Mode

async function generateTests(prompt: string): Promise<string> {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: [
        { role: 'system', content: '你是一个 TypeScript 测试代码生成器。只输出代码。' },
        { role: 'user', content: prompt }
      ],
      temperature: 0.2,  // 低温度保证输出稳定
      max_tokens: 2000
    })
  })

  const data = await response.json()
  return extractCodeBlock(data.choices[0].message.content)
}

function extractCodeBlock(raw: string): string {
  // 移除可能的 markdown 代码块包裹
  const match = raw.match(/```(?:typescript|ts)?\n?([\s\S]*?)```/)
  return (match?.[1] ?? raw).trim()
}

temperature: 0.2 是关键——我们需要确定性输出,不是创意文字。


六、阶段三:确定性校验 —— 不让 AI 代码直接进仓库

import { execSync } from 'child_process'
import fs from 'fs'

function validateGeneratedCode(filePath: string): boolean {
  // 1. TypeScript 编译检查
  try {
    execSync(`npx tsc --noEmit ${filePath}`, { stdio: 'pipe' })
  } catch {
    console.error(`❌ ${filePath} 编译失败`)
    return false
  }

  // 2. AST 完整性检查:生成的测试是否覆盖了所有导出函数
  const source = fs.readFileSync(filePath, 'utf-8')
  const testCount = (source.match(/it\(/g) || []).length

  if (testCount === 0) {
    console.error(`❌ ${filePath} 未生成任何测试用例`)
    return false
  }

  console.log(`✅ ${filePath} 通过校验 (${testCount} 个测试用例)`)
  return true
}

这段代码放在 pre-commit hook 中:AI 生成的测试如果不能通过 tsc --noEmit,commit 直接拒绝。


七、集成到 CI 管线

完整的工作流 CLI:

// gen-tests.ts —— 一键生成 + 校验
import { extractInterfaces } from './extractor'
import { buildPrompt, generateTests } from './generator'
import { validateGeneratedCode } from './validator'

async function main() {
  const interfaces = extractInterfaces('./src/types/api.ts')

  for (const [name, def] of interfaces) {
    const prompt = buildPrompt(name, def.members)
    const code = await generateTests(prompt)

    const outPath = `./src/__tests__/${name}.test.ts`
    fs.writeFileSync(outPath, code, 'utf-8')

    if (!validateGeneratedCode(outPath)) {
      process.exit(1)
    }
  }
}

main()

CI 配置一行搞定:

# .github/workflows/gen-tests.yml
- name: Generate API Tests
  run: npx tsx scripts/gen-tests.ts
  env:
    OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

八、关键设计决策总结

决策做法原因
输入源Compiler API 提取 AST,不传原始 JSON类型信息精确,不受 AI 幻觉影响
温度参数temperature: 0.2代码生成需要确定性,不是创意
校验层tsc --noEmit + 用例数量断言纯确定性检查,不依赖 AI
AI 角色只负责"模式填充"不做类型推断,不做逻辑决策
集成方式pre-commit hook / CI step每次生成后立即校验,不合格不进仓库

核心思想只有一句话:让 AI 干活,让编译器把关。AI 生成的内容如果通不过 tsc,就当它没生成过。这个原则让 ChatGPT 从"不可信的魔法"变成了"可工程化的生产力工具"。

0 评论

评论区

登录 后参与评论