You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:32:31