- 开发工具
【免费下载链接】ts-morph
TypeScript Compiler API wrapper for static analysis and programmatic code changes.
本文基于 ts-morph 官方文档 docs/details/modifiers.md,并结合仓库源码深入讲解修饰符(Modifier)的完整 API:如何获取、按语法类型查询、判断存在性以及切换private、async、export等修饰关键字。读完本文,你将掌握在静态分析与程序化代码修改中正确读写修饰符的全部手段,并理解其底层实现与边界行为。
什么是修饰符
在 TypeScript 中,修饰符是用于“修饰”其他节点的关键字节点。例如private关键字会改变类方法的可见作用域,async声明函数为异步,export决定模块是否对外暴露符号。在 ts-morph 的 AST 中,这些关键字被表示为Node<ts.Modifier>类型的节点,位于目标声明节点的modifiers列表里。
并不是所有节点都能拥有修饰符,只有满足条件的节点才会混入ModifierableNode,从而获得下面将要介绍的一整套修饰符操作函数。源码实现位于 ModifierableNode.ts,其类型定义明确了所有受支持的修饰符文本(ModifierTexts):
export type ModifierTexts = | "export" | "default" | "declare" | "abstract" | "public" | "protected" | "private" | "readonly" | "static" | "async" | "const" | "override" | "in" | "out" | "accessor";这 15 种文本覆盖了 TS 中绝大多数修饰关键字,也是hasModifier(text)、toggleModifier(text)等字符串重载所接受的值集合。
哪些节点拥有修饰符
从源码中可以看到,ModifierableNode被大量声明类混入,典型场景包括(对应文件位于 ast/base 目录):
- 类成员:方法 MethodDeclaration.ts、属性 PropertyDeclaration.ts、构造函数 ConstructorDeclaration.ts;
- 类声明:通过 ClassLikeDeclarationBase.ts 混入;
- 函数与变量:函数声明 FunctionDeclaration.ts(重载签名部分)、函数表达式 FunctionExpression.ts、变量声明语句 VariableStatement.ts;
- 模块与导入:模块声明 ModuleDeclaration.ts、导入等值声明 ImportEqualsDeclaration.ts;
- 枚举、接口与类型:枚举声明 EnumDeclaration.ts、接口声明 InterfaceDeclaration.ts、属性签名 PropertySignature.ts、索引签名 IndexSignatureDeclaration.ts、类型别名 TypeAliasDeclaration.ts、类型参数 TypeParameterDeclaration.ts、参数声明 ParameterDeclaration.ts 等。
因此,只要你在代码中操作上述任一节点,都可以直接调用getModifiers()等函数,无需先做类型断言。
读取全部修饰符:getModifiers
getModifiers()返回该节点全部修饰符组成的数组,元素为包装后的Node<ts.Modifier>对象,可以继续调用节点通用方法(如getText()、getKind())。
functionDeclaration.getModifiers();源码实现(ModifierableNode.ts)非常简单:
getModifiers() { return this.getCompilerModifiers().map(m => this._getNodeFromCompilerNode(m)) as Node<ts.Modifier>[]; }它先读取编译器节点上的modifiers数组(见私有方法getCompilerModifiers(),读取(this.compilerNode as any).modifiers ?? []),再把每个编译器节点包装成 ts-morph 的Node返回。测试用例验证了返回顺序与原始代码一致,例如对于export abstract class Identifier {},getModifiers()依次返回ExportKeyword与AbstractKeyword两个节点(见 modifierableNodeTests.ts)。
按语法类型获取第一个修饰符:getFirstModifierByKind 与 OrThrow 变体
当只需要判断某个特定关键字是否存在时,使用getFirstModifierByKind(syntaxKind: SyntaxKind):
functionDeclaration.getFirstModifierByKind(SyntaxKind.AsyncKeyword);例如,要拿到函数声明中的async关键字节点:
const asyncKeyword = functionDeclaration.getFirstModifierByKind(SyntaxKind.AsyncKeyword); if (asyncKeyword != null) console.log(asyncKeyword.getText()); // "async"其实现(ModifierableNode.ts)会遍历编译器修饰符,找到第一个kind匹配的节点并返回包装结果;找不到则返回undefined,而不是抛错。返回类型被精确映射为KindToNodeMappings[TKind],因此你能获得对应关键字节点的强类型(如Node<ts.AsyncKeyword>)。
如果希望“找不到就报错”,使用抛错版本:
const asyncKeyword = functionDeclaration.getFirstModifierByKindOrThrow(SyntaxKind.AsyncKeyword);getFirstModifierByKindOrThrow(ModifierableNode.ts)内部通过errors.throwIfNullOrUndefined实现,找不到时抛出默认错误信息Expected a modifier of syntax kind: AsyncKeyword,你也可以传入自定义 message。测试覆盖了“存在返回节点”与“不存在抛异常”两种路径(modifierableNodeTests.ts)。
判断是否拥有修饰符:hasModifier
hasModifier是唯一一个同时支持“语法类型”与“文本”两种参数的查询 API:
functionDeclaration.hasModifier(SyntaxKind.AsyncKeyword); // 按语法类型,返回 boolean functionDeclaration.hasModifier("async"); // 按修饰符文本,返回 boolean实现(ModifierableNode.ts)对字符串参数采用m.getText() === textOrKind精确比较,对SyntaxKind参数则用m.kind === textOrKind比较:
hasModifier(textOrKind: ModifierTexts | SyntaxKind) { if (typeof textOrKind === "string") return this.getModifiers().some(m => m.getText() === textOrKind); else return this.getCompilerModifiers().some(m => m.kind === textOrKind); }注意字符串比较是精确匹配的:hasModifier("async")与hasModifier(SyntaxKind.AsyncKeyword)语义等价,但传"async "(含空格)会返回false。测试同时验证了两种参数形式对存在与不存在两种情况的结果(modifierableNodeTests.ts)。
切换修饰符:toggleModifier
toggleModifier是修改类 API 中最常用、也最省心的一个。它根据当前状态自动决定添加或删除:
functionDeclaration.toggleModifier("async"); functionDeclaration.toggleModifier("async", false); // 显式指定:false 表示确保移除行为规则如下:
- 不传第二个参数:若当前没有该修饰符则添加,有则移除;
- 传
true:确保存在(幂等添加); - 传
false:确保移除(幂等删除)。
源码(ModifierableNode.ts)先解析默认值再委托给内部方法:
toggleModifier(text: ModifierTexts, value?: boolean) { if (value == null) value = !this.hasModifier(text); if (value) this.addModifier(text); else this.removeModifier(text); return this; }toggleModifier返回this,便于链式调用。测试用例完整覆盖了四象限(添加/移除 × 显式/隐式),并验证了两个重要边界:
- 切换时不会破坏 JSDoc 注释:对
/** test */declare function Test {}执行toggleModifier("declare", false)后文本变为/** test */function Test {}; - 切换时不会破坏行注释:对
// test\ndeclare function Test {}执行同样操作后保留// test行。
见 modifierableNodeTests.ts。
底层实现:addModifier 与 removeModifier
addModifier与removeModifier是toggleModifier的内部支撑,官方文档将它们标记为@internal(ModifierableNode.ts),意味着它们属于实现细节、不鼓励直接调用。但理解其机制有助于把握toggleModifier的行为。
添加时自动排序
addModifier并非简单地把关键字拼在节点开头,而是依据getAddAfterModifierTexts定义的修饰符顺序规则,把新修饰符插入到正确位置(ModifierableNode.ts)。核心规则可归纳为:
export总是最先出现(无前置要求);default、const跟在export之后;declare跟在export、default之后;static跟在public/protected/private之后;override跟在作用域关键字与static之后;abstract需放在export/default/declare以及作用域、static、override之后;async、readonly紧随abstract之后再追加;in在const之后,out在const、in之后;accessor排在作用域、declare、override、static、abstract、readonly之后。
测试演示了这种自动排序效果:对class Identifier {}依次addModifier("abstract")、addModifier("export")得到export abstract class Identifier {};依次添加export、abstract、declare得到export declare abstract class Identifier {}(modifierableNodeTests.ts)。
此外,addModifier还有两个细节:
- 幂等性:若节点已存在同文本修饰符,直接返回已有节点,不会重复添加(测试验证
export加两次仍只有一个); - 正确处理装饰器与 JSDoc:插入位置计算会跳过开头的装饰器语法列表与 JSDoc 注释节点,因此对
@dec class Identifier {}加export得到@dec export class Identifier {},对带/** Test */的声明同理(ModifierableNode.ts)。
删除时的注释安全
removeModifier通过removeChildren实现(ModifierableNode.ts)。当该节点只有一个修饰符时,会连同整个SyntaxList(修饰符列表节点)一起移除,并启用removeFollowingSpaces: true清理多余空格,同时避免误删前后的注释。这正是前面测试中断言注释得以保留的原因。
基于修饰符的高级封装:isAsync、setScope 等
ModifierableNode的实用价值还体现在它是多个高阶 mixin 的地基。以下两组 API 的内部实现全部依赖修饰符操作,可作为你在业务代码中复用“修饰符能力”的参考范式。
异步相关:AsyncableNode
AsyncableNode.ts 声明Node & ModifierableNode为扩展基座,提供了更语义化的接口:
functionDeclaration.isAsync(); // 是否 async functionDeclaration.getAsyncKeyword(); // async 关键字节点或 undefined functionDeclaration.getAsyncKeywordOrThrow(); // 找不到即抛错 functionDeclaration.setIsAsync(true); // 显式设置异步其实现全部委托给修饰符 API:isAsync()即hasModifier(SyntaxKind.AsyncKeyword),getAsyncKeyword()即getFirstModifierByKind(SyntaxKind.AsyncKeyword),setIsAsync(value)即toggleModifier("async", value)。
作用域相关:ScopedNode / ScopeableNode
作用域关键字(public/protected/private)是修饰符中最典型的应用。ts-morph 提供了 Scope.ts 枚举:
export enum Scope { Public = "public", Protected = "protected", Private = "private", }ScopedNode.ts 与 ScopeableNode.ts 均基于ModifierableNode实现getScope()/setScope(scope)/hasScopeKeyword()。其中setScopeForNode正是通过多次调用toggleModifier完成作用域切换:
export function setScopeForNode(node: Node & ModifierableNode, scope: Scope | undefined) { node.toggleModifier("public", scope === Scope.Public); // 始终显式 node.toggleModifier("protected", scope === Scope.Protected); node.toggleModifier("private", scope === Scope.Private); }而getScopeForNode则借助ts.getCombinedModifierFlags位运算判断当前作用域(ScopeableNode.ts)。这条能力链同时展示了本仓库中getCombinedModifierFlags()的通用入口,定义在 Node.ts,可用于获取节点全部修饰符的组合标志位。
实战示例:给类方法批量设置与切换修饰符
综合以上 API,下面给出一个完整的实操场景——把类中所有方法设为async,并确保其为public可见:
import { Project, SyntaxKind, Scope } from "ts-morph"; const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile( "src/example.ts", "class Service { doWork(): void {} }", ); const classDecl = sourceFile.getClasses()[0]; for (const method of classDecl.getMethods()) { // 1. 查询:按文本与语法类型判断 console.log(method.hasModifier("async")); // false console.log(method.getFirstModifierByKind(SyntaxKind.AsyncKeyword)); // undefined // 2. 切换:无则添加,返回 this 便于链式 method.toggleModifier("async"); // 添加 async method.setScope(Scope.Public); // 显式 public // 3. 校验 console.log(method.getModifiers().map(m => m.getText())); // ["public", "async"] console.log(method.hasModifier(SyntaxKind.PublicKeyword)); // true } console.log(sourceFile.getFullText()); // class Service { public async doWork(): void {} }输出结果符合预期:async被自动插入到public之后,符合getAddAfterModifierTexts中“async位于作用域关键字之后”的排序规则。
小结
- 修饰符是修饰其他节点的关键字节点,
private、async、export等都是典型例子; - 只有混入
ModifierableNode的节点才能调用相关 API,覆盖类成员、函数、变量、模块、接口、枚举、类型参数等绝大多数声明; - 读取与查询用
getModifiers()、getFirstModifierByKind()、getFirstModifierByKindOrThrow()、hasModifier()(支持文本或SyntaxKind两种入参); - 修改用
toggleModifier(text, value?),幂等且自动按规范排序、保留装饰器与注释;内部addModifier/removeModifier为@internal实现; - 更语义化的
isAsync()、setScope()等 API 均构建在修饰符操作之上,可直接作为业务代码的参考范式。
相关实现与测试可继续研读:ModifierableNode.ts、AsyncableNode.ts、ScopeableNode.ts、modifierableNodeTests.ts。
- 开发工具
【免费下载链接】ts-morph
TypeScript Compiler API wrapper for static analysis and programmatic code changes.
相关推荐
ts-morph 环境声明(Ambient)节点处理完全指南:isAmbient、declare 关键字检测与增删
ts morph 环境声明(Ambient)节点处理完全指南:isAmbient、declare 关键字检测与增删 摘要导读: 在 TypeScript 中,"
开发工具TypeScript 映射类型修饰符(Mapped Type Modifiers)完全指南:readonly、+readonly、-readonly 与可选性变换
TypeScript 映射类型修饰符(Mapped Type Modifiers)完全指南:readonly、+readonly、 readonly 与可选性变
文档教程Imba 事件修饰符(Event Modifiers)完全指南:内置修饰符、自定义修饰符与处理链机制
Imba 事件修饰符(Event Modifiers)完全指南:内置修饰符、自定义修饰符与处理链机制 事件修饰符(Event Modifiers)是 Imba
编程语言编译器语言运行时
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考