| name | ts-morph-refactor |
| description | TypeScript Compiler API via ts-morph for structural code manipulation. Navigate class/interface/function AST nodes, add/remove/rename declarations, batch refactor across a project, and type-safe code generation. Sources: dsherret/ts-morph (MIT). |
/ts-morph-refactor
When to Use
- Structurally refactor TypeScript code: rename interfaces, add method signatures, inject decorators
- Enforce architectural rules: every exported function must have a JSDoc tag
- Code generation: generate typed client SDKs from API schemas
- Safe batch rename across a full TypeScript project (respects type references)
Do NOT use for
- Runtime JS manipulation (use [[magic-string-sourcemap]] for source patching)
- Non-TypeScript codebases (use [[babel-ast-transform]] for plain JS)
Project setup
import { Project, SyntaxKind } from 'ts-morph'
const project = new Project({
tsConfigFilePath: 'tsconfig.json',
compilerOptions: { strict: true, target: 2 },
})
project.addSourceFilesAtPaths('src/**/*.ts')
Navigate and inspect
const sourceFiles = project.getSourceFiles()
for (const sf of sourceFiles) {
for (const fn of sf.getFunctions()) {
if (fn.isExported() && fn.isAsync() && !fn.getReturnTypeNode()) {
console.warn(
`[ts-morph] Missing return type: ${fn.getName()} in ${sf.getFilePath()}`
)
}
}
}
Add interface method signature
const sf = project.getSourceFileOrThrow('src/IAgent.ts')
const iface = sf.getInterfaceOrThrow('IAgent')
iface.addMethod({
name: 'shutdown',
returnType: 'Promise<void>',
docs: [{ description: 'Gracefully stop the agent.' }],
})
await sf.save()
Rename symbol across entire project
const sf = project.getSourceFileOrThrow('src/IAgent.ts')
const iface = sf.getInterfaceOrThrow('IAgent')
iface.rename('IAgentV2')
await project.save()
console.log('[ts-morph] Renamed IAgent → IAgentV2 across all files')
Generate TypeScript file from schema
function generateTypedClient(
methods: { name: string; params: string; returnType: string }[]
): void {
const sf = project.createSourceFile('src/generated/client.ts', '', {
overwrite: true,
})
const cls = sf.addClass({
name: 'GeneratedClient',
isExported: true,
})
for (const m of methods) {
cls.addMethod({
name: m.name,
isAsync: true,
parameters: [{ name: 'params', type: m.params }],
returnType: `Promise<${m.returnType}>`,
statements: `return this.call('${m.name}', params)`,
})
}
sf.saveSync()
}
Anti-Fake-Pass Checklist
❌ project.save() not called → mutations in memory only, files unchanged on disk
❌ getSourceFileOrThrow on non-existent file → throws; use getSourceFile + null check
❌ rename() without saving entire project → partial renames leave inconsistent references
❌ addSourceFilesAtPaths after manipulation → re-adding files resets unsaved changes
❌ compilerOptions mismatch with tsconfig → type-checker uses different rules than editor
❌ Large projects without skipAddingFilesFromTsConfig: true → loads all node_modules types, extremely slow