- 开发工具
【免费下载链接】ts-morph
TypeScript Compiler API wrapper for static analysis and programmatic code changes.
本篇文章以 ts-morph 仓库中的 classes 官方文档 为主线,系统讲解ClassDeclaration(类声明)的获取、命名、增删改查、继承关系分析、成员操作与接口提取等完整能力。读完本文,你将能够用 ts-morph 以类型安全的方式对类声明进行静态分析与程序化代码变更,并理解这些 API 在源码中的底层实现。
获取类声明
类声明可以从源文件(SourceFile)、命名空间(Namespace)或函数体中获取。ts-morph 将这些容器统一封装为"语句容器",getClasses()等查询方法定义在 StatementedNode.ts 中,其实现是对getStatements()结果按节点类型过滤:
const classes = sourceFile.getClasses(); const class1 = sourceFile.getClass("Class1"); const firstClassWithConstructor = sourceFile.getClass(c => c.getConstructors().length > 0);三种典型用法对应:
getClasses():返回全部ClassDeclaration[];getClass("Class1"):按名称精确匹配,返回ClassDeclaration | undefined;getClass(predicate):传入 find 函数做条件查询,返回第一个满足条件的声明或undefined。
类似地还有getClassOrThrow(),它会在找不到时抛出带有明确错误信息的异常。从源码看,getClass最终委托给getNodeByNameOrFindFunction(this.getClasses(), nameOrFindFunction)完成"按名或按函数"的查找。
注意:查询到的类声明(以及下文的成员、属性等)都支持"传字符串名字"或"传断言函数"两种参数形态,这是 ts-morph 的一贯设计。
名称(Name):允许匿名类
类声明可能没有名字,例如export default class {}这种匿名默认导出:
export default class { // etc... }因此,ClassDeclaration上的getName()与getNameNode()都返回可空类型(nullable)。在做依赖类名才能执行的操作(如查找派生类、提取接口时使用默认名称)时,源码中也会显式处理getName() == null的情形——例如 ClassDeclaration.ts 中getDefaultExtractedName会依次回退到类名、源文件名(去扩展名并清洗非法字符)。
添加与插入(Add / Insert)
通过addClass()、addClasses()、insertClass()、insertClasses()可以在源文件、命名空间或函数体内添加或插入类声明:
const classDeclaration = sourceFile.addClass({ name: "ClassName", });从 StatementedNode.ts 的实现可以看出,addClasses等价于"在语句数组末尾插入",insertClasses(index, ...)则支持指定子节点索引位置,插入逻辑统一走_insertChildren,并由structurePrinterFactory.forClassDeclaration(...)打印出最终文本。传入的结构体支持完整的ClassDeclarationStructure字段(name、extends、implements、ctors、methods、properties、getAccessors、setAccessors、staticBlocks、typeParameters、decorators、docs等),这些成员会在结构体设置阶段被逐一展开写入。
移除(Remove)
调用.remove()即可从源文件中删除该声明,ts-morph 会同步处理好格式化与前后空行:
classDeclaration.remove();Extends 表达式
getExtends()返回ExpressionWithTypeArguments | undefined,即extends子句的类型节点(可带类型实参):
const extendsExpression = classDeclaration.getExtends();设置 extends 表达式(支持字符串或WriterFunction):
classDeclaration.setExtends("BaseClass");移除 extends:
classDeclaration.removeExtends();在底层实现(ClassLikeDeclarationBase.ts)中,setExtends会优先定位已有的extendsHeritageClause 做原位替换;若不存在,则在implements子句之前或类体{之前插入extends <text>;而removeExtends本质是移除 extends 子句中的第一个类型节点。此外还有getExtendsOrThrow()供"必须存在"的场景使用。
Implements 表达式
getImplements()返回ExpressionWithTypeArguments[],即所有implements的类型节点数组:
const implementsExpressions = classDeclaration.getImplements();添加或插入 implements 表达式(支持单个字符串、字符串数组):
classDeclaration.addImplements("Named"); classDeclaration.addImplements(["Named", "Aged"]); classDeclaration.insertImplements(1, "Named"); classDeclaration.insertImplements(1, ["Named", "Aged"]);按索引或按节点移除某个表达式:
classDeclaration.removeImplements(0); // index classDeclaration.removeImplements(classDeclaration.getImplements()[0]); // nodeinsertImplements的index指的是 implements 表达式列表中的位置(从 0 开始)。
基类型(Base Types)
getBaseTypes()返回Type[],它基于类型检查器解析extends与implements声明的类型层面的父类型:
const baseTypes = classDeclaration.getBaseTypes(); // returns: Type[]源码实现(ClassLikeDeclarationBase.ts)就是this.getType().getBaseTypes()。文档特别强调:当类可能继承自 mixin(多个基类型交叉合并)时,应优先使用getBaseTypes(),因为 type 层面能返回完整的父类型集合,而不只是单个基类。
基类(Base Class)
getBaseClass()返回ClassDeclaration | undefined,它只关心"真正的类":
const baseClass = classDeclaration.getBaseClass(); // returns: ClassDeclaration | undefined实现上(ClassLikeDeclarationBase.ts),该方法先把基类型按交叉类型展开,收集其 symbol 的声明,然后只保留ClassDeclaration类型的声明;当恰好只有一个类声明时返回它,否则返回undefined。因此:
若基类可能是 mixin,请使用
.getBaseTypes(),而不是.getBaseClass()。
派生类(Derived Classes)
getDerivedClasses()返回所有继承自当前类的类声明(含间接派生,避免循环引用):
const derivedClasses = classDeclaration.getDerivedClasses();其实现(ClassLikeDeclarationBase.ts)先通过getImmediateDerivedClasses找到直接派生类:对类名做findReferencesAsNodes()引用查找,过滤出位于extendsHeritageClause 中、且父级为ClassDeclaration的节点;然后 BFS 式地逐层扩展所有间接派生类,同时用classes.indexOf(derivedClass) === -1防止循环继承造成死循环。注意:由于依赖findReferencesAsNodes,此方法要求类名节点存在(匿名类无法使用)。
构造函数(Constructor)
getConstructors()返回类中的构造函数声明数组:
const constructors = classDeclaration.getConstructors();关键语义(源码见 ClassLikeDeclarationBase.ts 与getAllMembers的过滤逻辑):
- ambient(环境声明)上下文:返回所有构造函数(包括重载签名);
- 非 ambient 上下文:仅返回实现构造函数(overload 签名会被过滤掉)。
添加或插入构造函数,使用addConstructor()、addConstructors()、insertConstructor()、insertConstructors():
const ctor = classDeclaration.addConstructor({ /* options like parameters may go here */ });构造函数结构体支持parameters、scope、returnType、docs、decorators等字段。例如:
classDeclaration.addConstructor({ parameters: [{ name: "value", type: "number", scope: Scope.Public }], });set()方法在设置ctors结构时,会先getConstructors().forEach(c => c.remove())清空再重新添加(ClassDeclaration.ts),实现"整体替换"。
方法(Methods)
获取实例方法(支持按名或按 find 函数):
const instanceMethods = classDeclaration.getInstanceMethods(); const myMethod = classDeclaration.getInstanceMethod("myMethod"); const firstMethodWith2Params = classDeclaration.getInstanceMethod(m => m.getParameters().length === 2);获取静态方法:
const staticMethods = classDeclaration.getStaticMethods(); const myStaticMethod = classDeclaration.getStaticMethod("myMethod"); const firstStaticMethodWith2Params = classDeclaration.getStaticMethod(m => m.getParameters().length === 2);添加 / 插入
const method = classDeclaration.addMethod({ isStatic: true, name: "myMethod", returnType: "string" });insertMethods(index, ...)、insertMethod、addMethod、addMethods四件套齐备。MethodDeclarationStructure支持isStatic、name、parameters、returnType、typeParameters、statements(函数体)、scope、isAbstract、isAsync、isGenerator、decorators、docs等字段。
移除
method.remove();属性(Properties)
获取实例属性——包含参数属性(parameter properties),即构造函数的public/private/readonly参数提升为的属性:
const instanceProperties = classDeclaration.getInstanceProperties(); const myProperty = classDeclaration.getInstanceProperty("myProperty"); const myStringProperty = classDeclaration.getInstanceProperty(p => Node.isPropertyDeclaration(p) && p.getType().getText() === "string");获取静态属性:
const staticProperties = classDeclaration.getStaticProperties(); const myStaticProperty = classDeclaration.getStaticProperty("myStaticProperty"); const myStaticStringProperty = classDeclaration.getStaticProperty(p => Node.isPropertyDeclaration(p) && p.getType().getText() === "string");添加 / 插入
添加或插入属性:
const property = classDeclaration.addProperty({ isStatic: true, name: "prop", type: "string", });添加 get 访问器:
const getAccessor = classDeclaration.addGetAccessor({ name: "someNumber", returnType: "number", statements: ["return 5;"], });添加 set 访问器:
const setAccessor = classDeclaration.addSetAccessor({ name: "someNumber", parameters: [{ name: "value", type: "number" }], statements: ["_someNumber = value;"], });以上均有对应的insertGetAccessors()/insertGetAccessor/addGetAccessors/addGetAccessor与insertSetAccessors()/insertSetAccessor/addSetAccessors/addSetAccessor变体(定义见 ClassLikeDeclarationBase.ts)。
移除
propertyDeclaration.remove();获取成员(Get members)
getMembers()返回全部成员,不区分静态或实例,包含构造函数与静态块(ClassStaticBlockDeclaration):
const allMembers = classDeclaration.getMembers();getInstanceMembers()返回实例成员(包含参数属性,不含构造函数):
const instanceMembers = classDeclaration.getInstanceMembers();getStaticMembers()返回静态成员(含静态块,不含构造函数与参数属性):
const staticMembers = classDeclaration.getStaticMembers();底层实现值得留意:getMembers()会过滤掉无函数体的重载方法/构造函数(ambient 上下文除外);getInstanceMembers()通过getMembersWithParameterProperties()把实现构造函数中的参数属性"提升"为成员(ClassLikeDeclarationBase.ts)。此外还有getMember(name)、getMemberOrThrow()、getMembersWithComments()(含注释类元素)等更细粒度的 API。
提取接口(Extracting an Interface)
extractInterface()可从类中提取出等价的接口声明结构体(InterfaceDeclarationStructure),用于重构"类 → 接口"或生成类型约束:
// the passed in name is optional and defaults to the class name const interfaceStructure = classDeclaration.extractInterface(`I${classDeclaration.getName()}`);提取静态部分的接口结构体:
const interfaceStructure = classDeclaration.extractStaticInterface(`${classDeclaration.getName()}Static`);从 ClassDeclaration.ts 的实现看,提取规则为:
extractInterface:仅收集public的实例成员——包括构造函数的 public 参数属性(转为PropertySignatureStructure)、public 属性、public 访问器(get/set 合并为只读或可写属性签名)、public 方法(重载展开为MethodSignatureStructure),并保留类的 JSDoc 与类型参数;extractStaticInterface:收集public 静态成员,并把构造函数转为ConstructSignature(returnType指向实例接口名);- 参数属性的 JSDoc 注释会从构造函数的
@param标签中提取并迁移到对应属性签名上; - 名字缺省时依次回退:传入名 → 类名 → 源文件基础名(清洗非法字符)。
抽象(Abstract)
类的节点(方法、访问器等成员)可以为抽象。AbstractableNodemixin(AbstractableNode.ts)为类及类成员提供了统一能力:
method.isAbstract(); // returns: boolean获取抽象关键字节点:
method.getAbstractKeyword(); // returns: node | undefined设置是否为抽象:
method.setIsAbstract(true); // set as abstract method.setIsAbstract(false); // set as not abstract实现上isAbstract()即getAbstractKeyword() != null;getAbstractKeyword()查找abstract修饰符;setIsAbstract通过toggleModifier("abstract", isAbstract)增删修饰符,并同步处理格式化。
构造函数 / 方法 / 属性:更深入的能力
文档提示:ConstructorDeclaration、MethodDeclaration、PropertyDeclaration各自实现了"函数类声明"(FunctionLikeDeclaration)上的通用能力,并叠加了各自专属能力:
- Constructors:
ConstructorDeclaration除具备函数式声明的参数、返回类型、重载、JSDoc、装饰器能力外,还带有scope(作用域:public / protected / private),支持isOverload()判断是否重载签名、isImplementation()判断是否实现体、isParameterProperty()判断参数属性等; - Methods:
MethodDeclaration支持isStatic、isAbstract、isAsync、isGenerator、作用域与重载等; - Properties:
PropertyDeclaration支持isStatic、isReadonly、hasQuestionToken(可选属性)、isAbstract、作用域、getType()等。
这些成员的具体方法非常丰富,实践中可通过 IDE 的自动补全(auto-complete)直接探索全部能力。
访问器互相查找(Get / Set Accessors)
如果存在对应的访问器,get 访问器可以反向找到配对的 set 访问器:
const setAccessor = getAccessor.getSetAccessor();set 访问器也可以反向找到配对的 get 访问器:
const getAccessor = setAccessor.getGetAccessor();这为"读写属性对"的联动分析(如检查某个属性是否只读、是否存在非对称访问器)提供了便捷入口。
小结
结合本文与源码可以确认,ts-morph 对类声明的支持是全方位的:查询(getClasses/getClass)、增删(addClass/insertClass/remove)、继承分析(getExtends/getImplements/getBaseTypes/getBaseClass/getDerivedClasses)、成员操作(方法、属性、访问器、构造函数、静态块)、抽象修饰以及接口提取(extractInterface/extractStaticInterface)。这些 API 的绝大多数能力集中在 ClassDeclaration.ts 与 ClassLikeDeclarationBase.ts 中,对应的行为验证可参考 classDeclarationTests.ts。如果你想系统掌握 ts-morph 中ExpressionWithTypeArguments(表达式与类型实参)、接口声明等相邻概念,可继续阅读 expressions 与 interfaces 文档。
- 开发工具
【免费下载链接】ts-morph
TypeScript Compiler API wrapper for static analysis and programmatic code changes.
相关推荐
ts-morph 修饰符(Modifiers)操作完全指南:读取、查询与切换类成员与声明的关键字
ts morph 修饰符(Modifiers)操作完全指南:读取、查询与切换类成员与声明的关键字 本文基于 ts morph 官方文档 docs/details
开发工具ts-morph 接口声明完全指南:检索、增删改查、继承分析与实现定位
ts morph 接口声明完全指南:检索、增删改查、继承分析与实现定位 本指南基于 ts morph 源码库中 docs/details/interfaces.
开发工具jynew MOD 模块设计:四种加载通道、描述文件规范与运行时启动全解析
jynew MOD 模块设计:四种加载通道、描述文件规范与运行时启动全解析 本文基于《jynew MOD模块设计》设计文档展开,结合 jyx2/Assets/S
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考