You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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.

Is this approach feasible?

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.

Step-by-Step Implementation Guide

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 your tsconfig.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);
    }
  });
}
Compiler API Reference & Tips

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 like SourceFile, Program, Printer, and SyntaxKind—they include detailed JSDoc comments explaining every property and method.
  • Official Examples: The TypeScript repo's tools directory 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.isXxx functions (e.g., ts.isVariableDeclaration) to identify node types safely.
    • Factory Functions: Always use ts.factory.createXxx to 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.
Full Working Example Script

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:08:38