将 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 评论
评论区
登录 后参与评论