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:
What does a
const
look like?How about a
let
?A simple
console.log
?How about adding two numbers?
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
Hygen | Hygen — Code Generation Framework based on EJS Templates
Using the Compiler API · microsoft/TypeScript Wiki (github.com)