☰
ts-morph 对象字面量表达式(Object Literal Expressions)操作指南:属性、访问器与方法的新增与删除
2026/10/10 8:34:36 网站建设 项目流程
  • 开发工具

【免费下载链接】ts-morph

TypeScript Compiler API wrapper for static analysis and programmatic code changes.

项目地址:https://gitcode.com/gh_mirrors/ts/ts-morph
点击查看免费下载

导读

对象字面量(Object Literal)是 JavaScript / TypeScript 中最常用的数据结构表达形式,而 ts-morph 作为 TypeScript 编译器 API 的封装库,为它提供了完整的“读取—修改—写出”能力。本文以 ts-morph 官方文档中 object-literal-expressions.md 为骨架,结合 ObjectLiteralExpression 源码 与 单元测试,系统讲解如何获取对象字面量的属性、如何通过结构(Structure)以编程方式新增各类成员(属性赋值、简写属性、展开赋值、get/set 访问器、方法)以及如何删除成员,并深入剖析其底层的插入与格式化原理,帮助读者在静态分析与代码生成场景中直接落地使用。

什么是对象字面量表达式

在 ts-morph 中,对象字面量表达式对应 TypeScript AST 中的ObjectLiteralExpression节点,其外观就是我们在代码中常见的{ ... }字面量。一个对象字面量可以混合包含多种成员,其典型形态如下:

const obj = { propertyAssignment: 5, shorthandPropertyAssignment, ...spreadAssignment, get getAccessor() { return 5; }, set setAccessor(value: number) { // do something }, method() { return "some string"; }, };

上述成员依次对应 ts-morph 中以下节点类型与结构:

成员形态节点类对应结构
key: valuePropertyAssignmentPropertyAssignmentStructure
key(简写属性)ShorthandPropertyAssignmentShorthandPropertyAssignmentStructure
...exprSpreadAssignmentSpreadAssignmentStructure
get key()/set key()GetAccessorDeclaration/SetAccessorDeclarationGetAccessorDeclarationStructure/SetAccessorDeclarationStructure
method()MethodDeclarationMethodDeclarationStructure

从源码结构看,所有可出现在对象字面量内部的成员都被统一为ObjectLiteralElementLike别名(见 aliases.ts),getProperties()的返回值即以此为类型。

获取 ObjectLiteralExpression 节点

在使用任何属性操作方法之前,首先需要拿到ObjectLiteralExpression节点本身。可以通过 AST 遍历接口来定位,例如从一个源码文件中查找:

import { SyntaxKind } from "ts-morph"; const objectLiteralExpression = sourceFile.getFirstDescendantByKindOrThrow(SyntaxKind.ObjectLiteralExpression);

测试代码中正是采用这种方式获取节点(见 objectLiteralExpressionTests.ts)。也可以从某个初始化的变量声明出发,先取到初始化表达式再向下获取:

const variableDeclaration = sourceFile.getVariableDeclarationOrThrow("obj"); const objectLiteralExpression = variableDeclaration.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);

更一般地,initializers 文档描述了如何从带初始化器的声明中定位表达式节点,对象字面量最常见的出现位置正是变量声明的初始化器。

读取对象的属性

拿到ObjectLiteralExpression节点后,读取属性有以下几种方式,对应 ObjectLiteralExpression.ts 中的实现:

// 获取全部属性(返回 ObjectLiteralElementLike[]) const properties = objectLiteralExpression.getProperties(); // 按名称获取第一个匹配的属性,找不到返回 undefined const property = objectLiteralExpression.getProperty("propertyAssignment"); // 用查找函数过滤,适合名称不便直接书写(如含扩展运算符)的情况 const spreadAssignment = objectLiteralExpression.getProperty( p => p.getText() === "...spreadAssignment", ); // 按名称获取,找不到时抛出异常 const method = objectLiteralExpression.getPropertyOrThrow("method");

getProperty的内部实现值得关注(源码 L56-L69):当传入字符串名称时,它会先检查成员上是否存在getName()方法(用于排除展开赋值等无名称成员),再逐一比较;当传入函数时则直接作为find谓词使用。这意味着getProperty既支持精确名称匹配,也支持任意复杂的自定义条件,例如按getKind()过滤:

const firstPropertyAssignment = objectLiteralExpression.getProperty( p => p.getKind() === SyntaxKind.PropertyAssignment, );

getPropertyOrThrow则在找不到时通过errors.throwIfNullOrUndefined抛出带名称的报错信息(源码 L37-L42)。测试用例验证了这些行为:名称匹配成功时返回节点文本、找不到时getProperty返回undefined而getPropertyOrThrow抛出异常(测试 L63-L100)。

关于注释的处理

需要注意的是,getProperties()只返回真实的 AST 成员,不包含注释。如果希望把对象内部的注释也作为可操作元素(例如在删除时保留或一并处理),应使用getPropertiesWithComments():

const propertiesWithComments = objectLiteralExpression.getPropertiesWithComments();

从实现看,该方法通过ExtendedParser.getContainerArray读取语法列表容器内的全部元素,包括注释节点(源码 L81-L84),返回类型为(ObjectLiteralElementLike | CommentObjectLiteralElement)[]。对应测试(L52-L61)展示了const t = {\n //a\n /*b*/\n};场景下getProperties()返回空数组而getPropertiesWithComments()返回两条注释文本。对注释语义感兴趣的读者还可进一步参考 comments 与 comment-ranges 文档。

新增对象成员:操作 API 总览

ts-morph 为每种成员类型都提供了add与insert两族方法,命名模式完全一致:

  • add 系列:追加到对象字面量的末尾,返回新节点;
  • insert 系列:在指定索引处插入,返回新节点。

每种成员的方法清单如下:

成员类型add 方法insert 方法
属性赋值addPropertyAssignment/addPropertyAssignmentsinsertPropertyAssignment/insertPropertyAssignments
简写属性addShorthandPropertyAssignment/addShorthandPropertyAssignmentsinsertShorthandPropertyAssignment/insertShorthandPropertyAssignments
展开赋值addSpreadAssignment/addSpreadAssignmentsinsertSpreadAssignment/insertSpreadAssignments
get 访问器addGetAccessor/addGetAccessorsinsertGetAccessor/insertGetAccessors
set 访问器addSetAccessor/addSetAccessorsinsertSetAccessor/insertSetAccessors
方法addMethod/addMethodsinsertMethod/insertMethods

此外还有一组通用方法addProperty/addProperties/insertProperty/insertProperties,它们接受统一的联合类型ObjectLiteralExpressionPropertyStructures(或字符串、WriterFunction),可混合插入任意成员。该联合类型定义于 aliases.ts:

export type ObjectLiteralExpressionPropertyStructures = | PropertyAssignmentStructure | ShorthandPropertyAssignmentStructure | SpreadAssignmentStructure | GetAccessorDeclarationStructure | SetAccessorDeclarationStructure | MethodDeclarationStructure;

add系列内部本质上是insert(index = 当前成员总数)的语法糖:源码中的#getAddIndex()返回当前容器成员数量(L87-L90),而addPropertyAssignment等均调用对应的insert族方法。

新增属性赋值(Property Assignment)

通过addPropertyAssignment传入PropertyAssignmentStructure即可:

const propertyAssignment = objectLiteralExpression.addPropertyAssignment({ name: "propertyAssignment", initializer: "5", });

PropertyAssignmentStructure的核心字段(见 PropertyAssignmentStructure.ts)为:

  • name:属性名,继承自PropertyNamedNodeStructure;
  • initializer:初始化器文本,类型为string | WriterFunction(可传字符串或写入函数,如writer => writer.write("5"))。

initializer支持字符串与WriterFunction两种形态,测试用例中就有{ name: "prop2", initializer: writer => writer.write("5") }的用法(测试 L273)。在索引位置插入的写法为:

objectLiteralExpression.insertPropertyAssignment(0, { name: "prop1", initializer: "4" });

对应测试验证了在开头、中间、末尾插入的文本结果(L269-L308)。

新增简写属性(Shorthand Property Assignment)

简写属性只需提供name,无需初始化器,这也是它与属性赋值在结构上的唯一差别(见 ShorthandPropertyAssignmentStructure.ts):

const shorthandPropertyAssignment = objectLiteralExpression.addShorthandPropertyAssignment({ name: "shorthandPropertyAssignment", });

多个简写属性可通过addShorthandPropertyAssignments/insertShorthandPropertyAssignments批量处理,测试中展示了中间插入两个简写属性的格式化效果(L372-L379)。

新增展开赋值(Spread Assignment)

展开赋值通过addSpreadAssignment添加,其结构继承自ExpressionedNodeStructure,核心字段是expression(展开目标表达式文本):

const spreadAssignment = objectLiteralExpression.addSpreadAssignment({ expression: "spreadAssignment" });

expression同样支持字符串或WriterFunction,例如测试中的{ expression: writer => writer.write("prop3") }(L448)。批量或按索引插入分别使用addSpreadAssignments/insertSpreadAssignments。

新增 get / set 访问器

get 与 set 访问器在结构上属于访问器声明(GetAccessorDeclarationStructure/SetAccessorDeclarationStructure),与类中的访问器共用结构类型,因此可以携带returnType、parameters、statements等字段:

const getAccessor = objectLiteralExpression.addGetAccessor({ name: "someNumber", returnType: "number", statements: ["return someNumber;"], }); const setAccessor = objectLiteralExpression.addSetAccessor({ name: "someNumber", parameters: [{ name: "value", type: "number" }], statements: ["someNumber = value;"], });

statements是写入函数体内的语句数组,逐行渲染到{ }中。对应测试验证了插入后的文本形如get prop1() {\n }(L588-L648)。注意这里isAmbient: false被固定写入结构打印器选项(见下文源码分析),因为对象字面量内的访问器不可能处于 ambient 环境。

新增方法(Method)

方法与访问器一样复用类成员的MethodDeclarationStructure:

const method = objectLiteralExpression.addMethod({ name: "method", statements: [`return "some string";`], });

同样支持insertMethod/insertMethods/addMethods,测试验证了批量插入多个方法的输出(L552-L558)。方法结构还可携带parameters、returnType、scope(对象字面量内无实际意义但结构兼容)、decorators等通用声明字段。

混合插入与注释

当需要一次性插入不同类型成员时,可以使用通用insertProperties/addProperties,甚至混入注释文本:

const result = objectLiteralExpression.addProperties([ { kind: StructureKind.PropertyAssignment, name: "p1", initializer: "5" }, { kind: StructureKind.ShorthandPropertyAssignment, name: "p2" }, { kind: StructureKind.SpreadAssignment, expression: "p3" }, { kind: StructureKind.Method, name: "m1", statements: ["return 1;"] }, "// 这是一条注释", writer => writer.write("// 通过 WriterFunction 写入的注释"), ]);

通用方法接受string | WriterFunction | ObjectLiteralExpressionPropertyStructures(或其数组),字符串与写入函数会作为原始文本直接输出。测试用例 L113-L146 验证了这种混合插入的完整输出格式。

删除对象成员

删除操作极为简洁:对获取到的成员节点调用.remove()即可。

const obj: ObjectLiteralExpression; // 假设已获取节点 obj.getPropertyOrThrow("prop1").remove();

.remove()是所有 ts-morph 节点通用的删除接口,其底层由RemoveChildrenTextManipulator/RemoveChildrenWithFormattingTextManipulator处理文本替换(相关实现见 manipulations/removal.ts),并自动处理逗号与换行的清理。测试验证了三种典型删除场景(L235-L257):

  • 属性独占一行时删除,const t = {\n prop1: 5\n};变为const t = {\n};;
  • 属性与花括号同行时删除,const t = { prop1: 5 };变为const t = { };;
  • 删除夹在两个属性之间的方法,其余成员保持正确逗号。

若需同时处理对象内部的注释,可先通过getPropertiesWithComments()拿到包含注释元素的列表,再逐个.remove()。

源码级原理:一次插入发生了什么

理解内部机制有助于预判格式化结果、调试复杂场景。以属性插入为例,insertPropertyAssignments最终调用私有方法#insertProperty(源码 L388-L406),其流程为:

  1. 索引校验:verifyAndGetIndex确保索引位于合法范围[0, 当前成员数];
  2. 文本生成:使用_getWriterWithChildIndentation()创建带缩进的写入器,再由CommaNewLineSeparatedStructuresPrinter按成员结构逐项打印代码;
  3. 文本插入:调用insertIntoCommaSeparatedNodes将生成的文本插入到SyntaxList语法列表中的指定位置;
  4. 节点重建:getNodesToReturn依据旧列表与新列表的差异重建包装节点,返回新成员对应的节点数组。

关键格式化行为由 manipulation settings 控制:

  • useTrailingCommas:是否在新成员后生成尾逗号,由project.manipulationSettings.getUseTrailingCommas()读取。测试 L148-L172 展示了开启尾逗号后p1,\n};的输出效果;
  • 缩进:新成员统一使用子级缩进层级,并与现有成员的换行风格对齐。

而各类型成员的打印则通过 ObjectLiteralExpressionPropertyStructurePrinter 按kind分发:PropertyAssignment、ShorthandPropertyAssignment、SpreadAssignment、Method、GetAccessor、SetAccessor分别交给对应的结构打印器,其中访问器与方法打印器固定使用{ isAmbient: false }选项。这也是为什么文档示例中只需给出name、initializer、expression等最小字段即可生成正确代码——其余细节由打印器按统一约定补齐。

完整实战:动态构造并修改一个对象

将上述 API 组合起来,可以完成一次完整的“读取—修改”闭环。以下示例基于Project读取内存中的源码并执行修改:

import { Project, StructureKind, SyntaxKind, ObjectLiteralExpression, } from "ts-morph"; const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile("test.ts", `const config = { name: "app", version, };`); const objectLiteralExpression = sourceFile.getFirstDescendantByKindOrThrow( SyntaxKind.ObjectLiteralExpression, ) as ObjectLiteralExpression; // 1. 读取 const nameProperty = objectLiteralExpression.getProperty("name"); console.log(nameProperty?.getText()); // name: "app" // 2. 修改已有属性初始化器 if (nameProperty?.isKind(SyntaxKind.PropertyAssignment)) nameProperty.setInitializer(`"my-app"`); // 3. 追加新成员 objectLiteralExpression.addPropertyAssignment({ name: "debug", initializer: "false", }); objectLiteralExpression.addShorthandPropertyAssignment({ name: "nodeEnv" }); objectLiteralExpression.addSpreadAssignment({ expression: "defaults" }); objectLiteralExpression.addGetAccessor({ name: "label", returnType: "string", statements: [`return this.name;`], }); objectLiteralExpression.addMethod({ name: "print", statements: ["console.log(this.label);"], }); // 4. 删除旧成员 objectLiteralExpression.getPropertyOrThrow("version").remove(); console.log(sourceFile.getFullText());

输出结果大致为:

const config = { name: "my-app", nodeEnv, ...defaults, get label() { return this.name; }, print() { console.log(this.label); }, debug: false };

通过getText()可以随时读取修改后的源码文本,再配合 emitting.md 中的emit与文件系统写入流程,即可将改动落盘。

小结与进一步阅读

对象字面量表达式操作是 ts-morph 表达式体系中最常用的一组能力:getProperties/getProperty/getPropertyOrThrow覆盖读取,按成员类型配对的add/insert族方法覆盖新增,通用的.remove()覆盖删除,而ObjectLiteralExpressionPropertyStructures联合类型让混合插入变得简洁。底层的结构打印器与insertIntoCommaSeparatedNodes机制保证了缩进、逗号与换行的正确性,且可通过manipulationSettings微调输出风格。

相关主题可继续深入:

  • expressions:表达式节点体系概览;
  • initializers:如何从变量声明等带初始化器的节点定位表达式;
  • object literal 相关节点源码:PropertyAssignment、ShorthandPropertyAssignment、SpreadAssignment等节点的完整实现;
  • structures 文档:结构对象(Structure)的通用设计规范;
  • settings 文档:useTrailingCommas等全局操作设置;
  • 对象字面量结构打印器:各成员类型的打印分发逻辑;
  • ObjectLiteralExpression 测试套件:覆盖本文所述全部 API 的行为验证。
  • 开发工具

【免费下载链接】ts-morph

TypeScript Compiler API wrapper for static analysis and programmatic code changes.

项目地址:https://gitcode.com/gh_mirrors/ts/ts-morph
点击查看免费下载

相关推荐

上一篇:LunaTranslator:3 步跑通的游戏翻译工具
下一篇:Table Calendar:Flutter日历组件终极配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询