Skip to content

TypeScriptASTTransformer

Nikolay Alipiev edited this page Jun 6, 2024 · 4 revisions

TypeScriptASTTransformer

Table of contents

  1. Overview
  2. API And Usage
  3. FormattingService

The TypeScriptASTTransformer utility is designed to be able to manipulate a TypeScript AST structure and apply different changes to it. It is platform agnostic and is only concerned with the modification of a ts.SourceFile. As such, it expects a source file as input and exposes multiple utility methods that stage changes for the source file. The changes are later applied consecutively during finalization.

Currently it can:

  • add new members to object literals
  • modify existing members of object literals
  • create new object literal expressions
  • prepend/append members to array literals
    • it supports an optional anchor element that it can prepend/append elements around
  • create new array literal expressions
  • create new import declarations
  • add identifiers to existing import declarations
  • detect collisions between existing import declarations
  • locate variable declarations by given name and type
  • look up a node's ancestor and check it against a condition
  • look up a ts.PropertyAssignment in an object literal
  • look up an identifier/element in an array literal
  • create a call expression of the form x.call<T>(args)
    • where the type argument and the method arguments are optional
  • transform the AST into source code and apply formatting

Methods for creating nodes

In the transformer, there are methods that can be used to create new nodes. They are wrappers around methods of the same/similar names in the ts.factory and are exposed for ease of use. They include:

  • createObjectLiteralExpression - creates a ts.ObjectLiteralExpression with a set of key-value pair properties
    • it has an optional transform delegate that can be used to mutate the object's properties' values to a ts.LiteralExpression, by default it will transform them to a ts.StringLiteral
    • the newly-created object literal can be on single or multiple lines
  • createArrayLiteralExpression - creates a ts.ArrayLiteralExpression with the provided elements
    • it supports both primitive and complex elements
    • the newly-created array literal can be on single or multiple lines
  • createCallExpression - creates a ts.CallExpression for a given identifier that calls a method

For example:

const typeArg = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
const arg = ts.factory.createNumericLiteral("5");
const callExpression = astTransformer.createCallExpression("x", "myGenericFunction", [typeArg], [arg]);

The callExpression variable will contain a ts.CallExpression node which if printed will look something like this:

x.myGenericFunction<number>(5);
  • createImportDeclaration - creates a node for a ts.ImportDeclaration that can be:
    • side effects imports - import "my-module;"
    • default imports - import X from "my-module";
    • imports with named bindings - import { X, Y... } from "my-module";

Methods for modifying the AST

Additionally, there are methods that can be used to request changes in the AST. All of them require a conditional predicate that is used when drilling down to the appropriate node. These methods include:

  • requestNewMemberInObjectLiteral - creates a request for an update in the AST that will add a new member (ts.PropertyAssignment) in an object literal expression
  • requestJsxMemberInObjectLiteral - similar to requestNewMemberInObjectLiteral with the only difference being that the value of the created member will be a ts.JsxSelfClosingElement
  • requestUpdateForObjectLiteralMember - creates a request for an update in the AST that will change the value of a member in an object literal
  • requestNewMembersInArrayLiteral - creates a request for an update in the AST that will add n members in a particular ts.ArrayLiteralExpression
    • supports an optional anchorElement that can be used to prepend/append the new nods around
  • requestNewImportDeclaration - creates a request for an update in the AST that will add a new import declaration of the forms outlined in createImportDeclaration

An example usage of createObjectLiteralExpression alongside requestNewMembersInArrayLiteral can be:

const newObjectLiteral = this.astTransformer.createObjectLiteralExpression([
  { name: "path", value: ts.factory.createStringLiteral("some-new-path") },
  { name: "component", value: ts.factory.createIdentifier("MyComponent") },
]);

// the condition that will be used when traversing the AST
const condition = (node: ts.ArrayLiteralExpression) =>
  node.elements.some((e) => ts.isObjectLiteralExpression(e)
     && e.properties.some((p) => ts.isPropertyAssignment(p) && p.name.getText() === "path")
  );

this.astTransformer.requestNewMembersInArrayLiteral(condition, [newObjectLiteral]);

This will create a new object ({ path: "some-new-path", component: MyComponent }) and add it to an array literal that has an object literal member with property with a value path.

So this:

const routes: Route[] = [{ path: "some-path", component: SomeComponent }];

Will become this:

const routes: Route[] = [
  { path: "some-path", component: SomeComponent },
  { path: "some-new-path", component: MyComponent }
];

Keep in mind that this is a very crude and simplified example as in reality additional checks will have to be done to make sure that the node that is being added goes precisely where it is supposed to.

Additionally, regarding any of the exposed utilities, the transformer will not attempt to fix potentially broken code as it is only concerned with the modification of the AST and not whether or not the resulting code is actually runnable.

Updating the source file

The TypeScriptASTTransformer will only store the requested changes and will not apply any of them until either finalize or applyChanges is called.

  • applyChanges - applies the aggregated changes to the ts.SourceFile and returns the resulting AST, does not modify the original one
  • finalize - calls applyChanges internally and then prints the resulting source code, a formatter can be used to make the code prettier

Note

Calling either of these will clear the transformer's cache of requested changes.

The TypeScriptFormattingService is a utility used by the transformer after applying the changes to the AST and finalizing the source. This service will read a project's .editorconfig and attempt to format the parsed AST by using the ts.LanguageService's formatting capabilities. It is a completely standalone utility that can be replaced with a custom one if needed.