Home     /Articles     /

Exploring the TypeScript Compiler API: Custom Transformations Made Simple

Typescript

Exploring the TypeScript Compiler API: Custom Transformations Made Simple

Written by Briann     |

December 08, 2024     |

1.4k |

TypeScript isn’t just a superset of JavaScript; it's a powerful tool that provides a wide range of capabilities beyond basic type checking. One of its most advanced and lesser-known features is the TypeScript Compiler API. This API allows developers to manipulate TypeScript code programmatically, enabling custom transformations, linting, and code analysis.


In this post, we’ll explore the TypeScript Compiler API, its key features, and how to use it for custom transformations.





1. What Is the TypeScript Compiler API?

The TypeScript Compiler API exposes the inner workings of the TypeScript compiler (tsc). You can:

  • Parse and analyze TypeScript code.
  • Manipulate the Abstract Syntax Tree (AST).
  • Transform TypeScript to JavaScript with custom rules.


This API is ideal for creating tools like linters, formatters, and code generation utilities.





2. Why Use the Compiler API?

  • Custom Transformations: Modify code during the compilation process.
  • Code Analysis: Analyze code for patterns or optimization opportunities.
  • Tooling: Build tools for automated refactoring or enforcing coding standards.





3. Setting Up a Project for the TypeScript Compiler API

Step 1: Install TypeScript

Ensure TypeScript is installed in your project:

npm install typescript --save-dev


Step 2: Import the Compiler API

Use the typescript package to access the Compiler API:

import * as ts from "typescript";






4. Parsing TypeScript Code

The first step in working with the Compiler API is parsing TypeScript code into an AST.


Example: Parsing Code

import * as ts from "typescript";

const code = `const greet = (name: string): string => {return \`Hello, \${name}!\`; };`;

const sourceFile = ts.createSourceFile(
  "example.ts",
  code,
  ts.ScriptTarget.Latest,
  true
);

console.log(sourceFile);
  • ts.createSourceFile parses the code into an AST.
  • The resulting AST can now be traversed or manipulated.





5. Traversing the AST

Once you have the AST, you can traverse it to analyze or modify specific nodes.


Example: Traversing the AST

function visit(node: ts.Node) {
  if (ts.isVariableDeclaration(node)) {
    console.log("Variable:", node.name.getText());
  }
  node.forEachChild(visit);
}

visit(sourceFile);
  • ts.isVariableDeclaration checks the type of a node.
  • node.forEachChild recursively visits child nodes.





6. Creating Custom Transformations

Custom transformations allow you to modify TypeScript code before it’s compiled.


Example: Adding a Console Log to Functions

const transformer: ts.TransformerFactory = (context) => {
  return (sourceFile) => {
    function visit(node: ts.Node): ts.Node {
      if (ts.isFunctionDeclaration(node) && node.body) {
        const logStatement = ts.factory.createExpressionStatement(
          ts.factory.createCallExpression(
            ts.factory.createPropertyAccessExpression(
              ts.factory.createIdentifier("console"),
              "log"
            ),
            undefined,
            [ts.factory.createStringLiteral(`Function ${node.name?.getText()} called`)]
          )
        );

        const updatedBody = ts.factory.updateBlock(node.body, [
          logStatement,
          ...node.body.statements,
        ]);

        return ts.factory.updateFunctionDeclaration(
          node,
          node.decorators,
          node.modifiers,
          node.asteriskToken,
          node.name,
          node.typeParameters,
          node.parameters,
          node.type,
          updatedBody
        );
      }
      return ts.visitEachChild(node, visit, context);
    }
    return ts.visitNode(sourceFile, visit);
  };
};

// Apply the transformation
const result = ts.transform(sourceFile, [transformer]);
const transformedSourceFile = result.transformed[0];

const printer = ts.createPrinter();
console.log(printer.printFile(transformedSourceFile));
  • ts.factory creates new nodes or modifies existing ones.
  • The transformer inserts a console.log statement at the beginning of every function.





7. Emitting Transformed Code

Once transformations are applied, the updated AST can be emitted as TypeScript or JavaScript code.


Example: Emitting Transformed Code

const printer = ts.createPrinter();
const transformedCode = printer.printFile(transformedSourceFile);

console.log("Transformed Code:");
console.log(transformedCode);
  • ts.createPrinter converts the AST back to code.





8. Real-World Applications of the TypeScript Compiler API

  • Custom Linters: Analyze code to enforce project-specific conventions.
  • Code Generators: Automatically generate boilerplate code.
  • Build-Time Optimizations: Remove unused imports or inject performance optimizations.
  • AST Visualization: Build tools to visualize code structure for educational purposes.





9. Tips for Working with the Compiler API

  • Study the AST: Use tools like AST Explorer to understand node structures.
  • Start Small: Begin with simple transformations and build complexity incrementally.
  • Use Type Guards: Utilize ts.isXYZ functions to safely check node types.
  • Explore the Documentation: Refer to the TypeScript Compiler API documentation.





Conclusion

The TypeScript Compiler API opens up a world of possibilities for custom tooling, code analysis, and transformations. While it has a steep learning curve, mastering it can lead to powerful automation and enhanced development workflows.


Whether you’re building a custom linter, automating refactoring, or analyzing code for patterns, the Compiler API is a tool worth exploring. Start experimenting today, and unlock the full potential of TypeScript! 🚀

Powered by Froala Editor

Related Articles