Writing a TypeScript Code Generator: Templates vs AST

Code generators replace mindless coding with productivity; a pure function that produces predictable code, given a (set of) input(s). Generating boilerplate classes or templates — a common use case for Codegen.

Use-case: Generating Classes

We will be generating components (Class) that follow a repetitive yet predictable pattern.

export class HttpBinApi implements ICredentialType {
 name = 'httpbinApi';
 displayName = 'HttpBin API';
}

Notice how many times the developer has to rewrite “HTTP Bin API” in different cases and styles? Moreover, using the wrong case would cause linter issues and never-ending headaches. Could this be automated?

Approach: 🧵 String Manipulation

Simply using JavaScript String functions can take us a long way. Template Strings, joins and possibly a little bit of lodash .

function generateClass(name: string) {
    return [
        `export class ${pascalCase(name)}}Api implements ICredentialType {`,
        `  name = '${camelCase(name)}Api';`,
        `  displayName = '${titleCase(name)} API';`,
        `}`,
    ].join('\n');
}

While this works, it makes me uncomfortable; my mind implicitly sees fragility in this approach. What if the code generator is more complex? What if we need to iterate through arrays or objects? What if the signatures change? It seems unmaintainable.

Hygen

A more practical approach to string manipulation and using templates to generate code — could be Hygen. It handles file creation, manipulation of existing files gracefully.

The devs call it the “The scalable code generator that saves you time.” It is a code generation utility that integrates seamlessly into your development flow. It uses EJS templates to define your file structures.

---
to: src/credentials/<%= h.changeCase.pascal(name) %>.ts
---

export class <%= h.changeCase.pascal(name) %> implements ICredentialType {
    name = "<%= h.changeCase.camel(name) %>";
    displayName = "<%= name %>";
}

Check it out here: Hygen

Manipulating whitespace dependent languages like YAML or Python could be challenging with Hygen. — A dev who tried.

Let’s admit it, in our mind, “code generation is supposed to be a bit more complicated”. The simplicity here may be causing the nerves!

Approach: 🌲 Abstract Syntax Trees

Introduction

What if we could have a more programmatic-way of generating code? Instead of having apparently meaningless string concatenations, we had semantics and meanings?

Introducing ASTs. Code compilers usually do not compile strings of code directly. Instead, it can build up a tree-like structure of branches and objects and conditions.

A tree representation of a simple TypeScript class

Example of AST

For the HttpBinApi class mentioned at the top, the AST representation may look something like this:


  factory.createClassDeclaration(
    [factory.createToken(ts.SyntaxKind.ExportKeyword)],
    factory.createIdentifier("HttpBinApi"),
    //...
    [
      factory.createPropertyDeclaration(
        factory.createIdentifier("name"),
        //...
        factory.createStringLiteral("httpbinApi")
      ),
      factory.createPropertyDeclaration(
        factory.createIdentifier("displayName"),
         //...
        factory.createStringLiteral("HttpBin API")
      )
    ]
  );

Do check out the TypeScript AST Viewer to play around with this Syntax Tree.

Exploring ASTs on our own 🕵🏽

TypeScript AST Viewer in Action

Okay we went from string manipulation to extra-terrestrial programming in a paragraph. I encourage you to use the AST Viewer and click around for now. A few questions to get you started:

  1. What does a const look like?

  2. How about a let ?

  3. A simple console.log ?

  4. How about adding two numbers?

  5. Okay, looping through an array of numbers?

AST to Code? 👩🏻‍💻

All along, we’ve been converting TypeScript code to AST. But how do we do the opposite? Reconstructing TypeScript code is easy with the help of the built-in ts.Printer .

Step 1: Create a Printer.

const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });

Step 2: Create an Instance of a SourceFile .

const sourceFile = ts.createSourceFile(
    'HttpBinApi.ts',
    '',
    ts.ScriptTarget.Latest,
    false, // setParentNodes
    ts.ScriptKind.TS,
  );

Step 3: Render as String.

printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);

Check out a fully working, in-the-browser example right here: StackBlitz #1. Do feel free to play around with it!

Where is the Code-Generator? ⚙️

Ah yes yes, now we can assemble everything together into a semi-working code generator.

Step 1: Parametrised the AST representation.

const className = `${_.upperFirst(_.camelCase(name))}Api`;
const classIdentifier = factory.createIdentifier(className);

Step 2: With my trusty Co-Pilot, a large language model — I have refactored the code to make more legible.

Check out the refactored version: StackBlitz #2.

console.log(generateClass('Cloud File Transfer'));
❯ ts-node index.ts
export class CloudFileTransferApi implements ICredentialType {
    name = "cloudFileTransferApi";
    displayName = "Cloud File Transfer";
}

There are no clear-cut winners here. Based on your requirements, you do need to make a call on simplicity vs robustness. Hope you had fun! 🎉

References and Links to Check-out

  1. Hygen | Hygen — Code Generation Framework based on EJS Templates

  2. Using the Compiler API · microsoft/TypeScript Wiki (github.com)

  3. Typescript Code Generation — DEV Community

  4. TypeScript AST Viewer (ts-ast-viewer.com)

  5. AST explorer

  6. TypeScript AST Exploration — StackBlitz

  7. TypeScript AST Exploration: Parametrizing — StackBlitz