如何用TypeScript Compiler API比较项目双版本类型并判定变更等级?
TypeScript Compiler API跨程序类型比较与语义化版本判断问题
我有一个TypeScript脚本,原本通过ESLint实现项目转译、提取各类声明(导出、导入、类、接口、类型、函数、方法)及其类型参数、参数与类型值,用于生成代码片段和自动注入JSDoc。近期重构后改用TypeScript Compiler API,目标是对比项目新旧版本快照,深度比较每个提取节点,并基于语义化版本规则确定变更类型(PATCH、MINOR、MAJOR)。
变更规则示例
// SAME:类型完全兼容 type OLDER = string; type NEWEST = string; // SAME:结构一致的类类型兼容 class A { hello(): string { return "hello world"; } } class B { hello(): string { return "hello world"; } } type OLDER = A; type NEWEST = B; // MINOR:新类型是旧类型的超集(兼容旧类型输入) type OLDER = string; type NEWEST = string | number; // MINOR:any兼容所有类型 type OLDER = string; type NEWEST = any; // MAJOR:类型不兼容,旧类型无法赋值给新类型 type OLDER = string; type NEWEST = number | null;
当前实现代码框架
// 用TypeScript Compiler API提取项目声明 const olderProject = await Project.process(Path.join(__dirname, "./older/index.ts"), compilerOptions, resolutionHost); const newestProject = await Project.process(Path.join(__dirname, "./newest/index.ts"), compilerOptions, resolutionHost); // 新旧项目分属不同程序,拥有独立的TypeChecker const newestChecker = newestProject.program.getTypeChecker(); const olderChecker = olderProject.program.getTypeChecker(); // 提取名为Test的类型 const olderType = (() => { for (const file of olderProject.files) { for (const declaration of file.declarations) { if (declaration.isImport()) continue; if (declaration.name === "Test") { return olderChecker.getTypeAtLocation(declaration.source); } } } })(); const newestType = (() => { for (const file of newestProject.files) { for (const declaration of file.declarations) { if (declaration.isImport()) continue; if (declaration.name === "Test") { return newestChecker.getTypeAtLocation(declaration.source); } } } })(); // 目标:实现类型对比函数 enum Change { SAME = "SAME", MINOR = "MINOR", MAJOR = "MAJOR" } const compare = (older: Type, newest: Type): Change => { // ... 待实现逻辑 };
遇到的问题
- 递归类型(如
DeepArray<T> = T[] | DeepArray<T>)会导致遍历循环 - 跨不同程序的类型无法直接用TypeChecker的内置方法判断兼容性
- 类型场景覆盖复杂度高,担心遗漏边缘情况
请问是否有简便方法,通过TypeScript Compiler API判断类型A是否满足类型B,即便两者来自不同的程序?
解决方案
1. 跨程序类型兼容性判断:类型序列化+跨程序解析
由于新旧类型分属不同Program,无法直接用同一Checker做兼容性检查,核心思路是将其中一个类型序列化,在另一个程序中重新解析为本地类型,再用该程序的Checker判断兼容性:
/** * 将旧程序的类型序列化,在新程序中解析为本地类型 */ function getTypeInNewProgram(olderType: ts.Type, olderChecker: ts.TypeChecker, newestProject: Project): ts.Type { // 序列化旧类型为字符串 const typeStr = olderChecker.typeToString(olderType); // 创建临时源文件注入类型声明 const tempSourceFile = newestProject.createSourceFile("temp-type.ts", `type TempType = ${typeStr};`); // 获取临时类型 const tempDeclaration = tempSourceFile.getTypeAlias("TempType")!; return newestProject.program.getTypeChecker().getTypeAtLocation(tempDeclaration.getNameNode()); }
之后就可以用新程序的Checker判断兼容性:
const newestChecker = newestProject.program.getTypeChecker(); const olderTypeInNew = getTypeInNewProgram(olderType, olderChecker, newestProject); const oldAssignableToNew = newestChecker.isTypeAssignableTo(olderTypeInNew, newestType); const newAssignableToOld = newestChecker.isTypeAssignableTo(newestType, olderTypeInNew);
2. 递归类型循环问题:跟踪已访问类型
在类型遍历或序列化时,用Type.id(TypeScript内部唯一标识)跟踪已处理的类型,避免循环:
function serializeType(type: ts.Type, checker: ts.TypeChecker, visited: Set<number> = new Set()): string { if (visited.has(type.id)) return "..."; // 递归终止,避免循环 visited.add(type.id); // 处理联合类型 if (type.isUnion()) { return type.types.map(t => serializeType(t, checker, visited)).join(" | "); } // 处理交叉类型、泛型等其他类型分支... return checker.typeToString(type); }
3. 语义化版本判断逻辑映射
基于兼容性检查结果,对应到变更类型:
const compare = (older: ts.Type, newest: ts.Type, olderChecker: ts.TypeChecker, newestProject: Project): Change => { const newestChecker = newestProject.program.getTypeChecker(); const olderTypeInNew = getTypeInNewProgram(older, olderChecker, newestProject); const oldAssignableToNew = newestChecker.isTypeAssignableTo(olderTypeInNew, newest); const newAssignableToOld = newestChecker.isTypeAssignableTo(newest, olderTypeInNew); if (oldAssignableToNew && newAssignableToOld) { return Change.SAME; } if (oldAssignableToNew && !newAssignableToOld) { // 旧类型可赋值给新类型,属于兼容扩展(MINOR) return Change.MINOR; } // 其余情况均为不兼容变更(MAJOR) return Change.MAJOR; };
补充说明
any类型会被自动处理:any可赋值给任何类型,任何类型也可赋值给any,符合你示例中string→any判定为MINOR的规则- 类类型的结构兼容性:TypeScript Compiler API的
isTypeAssignableTo会自动识别结构一致的类,无需额外逻辑 - 特殊场景覆盖:可结合
getTypeFlags处理null/undefined、字面量类型、泛型参数等边缘情况
内容的提问来源于stack exchange,提问作者jeremie dupas
相关产品推荐
相关产品推荐

