如何用TypeScript Compiler API构建TS代码转换工具并输出TS?
Absolutely feasible—this is exactly the kind of workflow the TypeScript Compiler API was built for. Teams regularly use it for one-off codebase upgrades: think syntax migrations, enforcing new coding standards, or bulk refactoring patterns, all while keeping everything in TypeScript.
Yes, 100%. The TypeScript Compiler API isn't limited to transpiling TS to JS—it provides full access to parse TypeScript into an Abstract Syntax Tree (AST), modify that AST, and emit valid TypeScript code right back out. This makes it perfect for your one-time upgrade use case.
Here's how to build your transformation tool from scratch:
1. Set Up Your Environment
First, install the TypeScript package (it includes the Compiler API out of the box—no extra dependencies needed):
npm install typescript --save-dev
2. Parse Source Code into an AST
You have two primary ways to parse TypeScript code:
ts.createSourceFile: Ideal for single, isolated files (no dependency resolution or type checking). Great for quick tests or simple file transformations.ts.createProgram: Best for full codebases—respects yourtsconfig.json, resolves imports, and gives access to type checking capabilities. Use this if your transformation depends on type information or needs to handle inter-file dependencies.
Example using createProgram for a project:
import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; // Load your project's tsconfig const tsConfigPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json'); const tsConfig = ts.readConfigFile(tsConfigPath!, ts.sys.readFile); const program = ts.createProgram({ rootNames: tsConfig.config.include.map(file => path.resolve(file)), options: tsConfig.config.compilerOptions });
3. Traverse and Modify the AST
Use TypeScript's visitor pattern to walk the AST and make targeted changes. You'll define a visitor function that checks for specific node types, modifies them, and recurses into child nodes.
For example, here's a transformation that replaces all var declarations with let:
function transformVarToLet(sourceFile: ts.SourceFile): ts.SourceFile { const visitor: ts.Visitor = (node) => { // Check if we're looking at a `var` declaration list if (ts.isVariableDeclarationList(node) && node.kind === ts.SyntaxKind.VarDeclarationList) { // Create a new `let` declaration list with the same variable declarations return ts.factory.createVariableDeclarationList( node.declarations, ts.SyntaxKind.LetDeclarationList ); } // Recurse into child nodes to apply the transformation everywhere return ts.visitEachChild(node, visitor, ts.createCompilerHost({})); }; // Apply the visitor to the entire source file AST return ts.visitNode(sourceFile, visitor); }
4. Emit the Modified TypeScript Code
Use ts.createPrinter to convert the modified AST back into human-readable TypeScript. Configure it to preserve original formatting (whitespace, comments) to minimize noise in your codebase:
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, preserveWhitespace: true, // Keep original indentation and spacing removeComments: false, // Retain all existing comments }); // Process every source file in the program program.getSourceFiles().forEach(sourceFile => { if (!sourceFile.isDeclarationFile) { // Skip .d.ts files const transformedSource = transformVarToLet(sourceFile); const outputCode = printer.printNode(ts.EmitHint.SourceFile, transformedSource, sourceFile); // Write the modified code back to the original file (or output to a new directory for safety) fs.writeFileSync(sourceFile.fileName, outputCode); } });
5. Batch Process Your Entire Codebase
If you need to process files not covered by your tsconfig, or want to test changes without overwriting originals, add a directory traversal function:
function traverseDirectory(dir: string) { fs.readdirSync(dir).forEach(file => { const fullPath = path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { traverseDirectory(fullPath); } else if (fullPath.match(/\.(ts|tsx)$/)) { // Process single file with createSourceFile const content = fs.readFileSync(fullPath, 'utf8'); const sourceFile = ts.createSourceFile( fullPath, content, ts.ScriptTarget.Latest, true ); const transformed = transformVarToLet(sourceFile); const output = printer.printNode(ts.EmitHint.SourceFile, transformed, sourceFile); // Write to a new directory instead of overwriting const outputPath = fullPath.replace('./src', './src-transformed'); fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, output); } }); }
The official docs can feel scattered, but these resources will help you navigate:
- Type Definitions: The most comprehensive reference is the built-in types in
node_modules/typescript/lib/typescript.d.ts. Search for key types likeSourceFile,Program,Printer, andSyntaxKind—they include detailed JSDoc comments explaining every property and method. - Official Examples: The TypeScript repo's
toolsdirectory contains scripts that use the Compiler API for real-world tasks (like refactoring). These are great for seeing how complex transformations are implemented. - Core Concepts to Learn:
- AST Node Checks: Use
ts.isXxxfunctions (e.g.,ts.isVariableDeclaration) to identify node types safely. - Factory Functions: Always use
ts.factory.createXxxto create new AST nodes—this ensures your nodes are valid and compatible with TypeScript's internal structure. - Type Checking: If your transformation depends on type information (e.g., modifying only variables of a specific type), use
program.getTypeChecker()to access type metadata.
- AST Node Checks: Use
Here's a minimal, runnable script that transforms all var declarations to let in your src directory:
import * as ts from 'typescript'; import * as fs from 'fs'; import * as path from 'path'; // Transformation logic function transformVarToLet(sourceFile: ts.SourceFile): ts.SourceFile { const visitor: ts.Visitor = (node) => { if (ts.isVariableDeclarationList(node) && node.kind === ts.SyntaxKind.VarDeclarationList) { return ts.factory.createVariableDeclarationList( node.declarations, ts.SyntaxKind.LetDeclarationList ); } return ts.visitEachChild(node, visitor, ts.createCompilerHost({})); }; return ts.visitNode(sourceFile, visitor); } // Main execution function main() { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, preserveWhitespace: true, removeComments: false }); // Process all .ts/.tsx files in ./src traverseDirectory(path.resolve('./src')); function traverseDirectory(dir: string) { fs.readdirSync(dir).forEach(file => { const fullPath = path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { traverseDirectory(fullPath); } else if (fullPath.match(/\.(ts|tsx)$/)) { const content = fs.readFileSync(fullPath, 'utf8'); const sourceFile = ts.createSourceFile( fullPath, content, ts.ScriptTarget.Latest, true ); const transformed = transformVarToLet(sourceFile); const output = printer.printNode(ts.EmitHint.SourceFile, transformed, sourceFile); // Write to a transformed directory (safe testing) const outputPath = fullPath.replace('./src', './src-transformed'); fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, output); console.log(`Transformed: ${fullPath}`); } }); } } main();
内容的提问来源于stack exchange,提问作者snort

