Do you ever wonder how to generate TypeScript code automatically?
Using the TypeScript Compiler API, we can leverage it's factory functions for writing scripts that can generate TypeScript code, based on any rules we want!
To get started, create a new project with npm init
then install the "typescript" npm package with npm install typescript
. Create a file index.mjs
with touch index.mjs
which will hold our code generation script.
Next, we want to figure out what kind of code we want to generate. For this example, we will generate the following function:
function add(a: number, b: number): number {
return a + b;
}
This is a very simple example, but the concepts apply to any type of code you want to generate.
Inside index.mjs
, import the "typescript" package and start exploring what is available in it via intellisense. For this example, we see that we will need to define a few "identifiers", specifically a
, b
and add
. Those are all "names" and are represented by identifiers inside Abstract Syntax Trees.
We can start by defining each identifier at the top of our file using the factory method called createIdentifier
:
const aId = ts.factory.createIdentifier("a");
const bId = ts.factory.createIdentifier("b");
const addId = ts.factory.createIdentifier("add");
We'll also need the number
keyword a few times so we can add that right below the others:
const numberKeyword = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
Then we can start creating the function declaration and use intellisense to figure out what needs to be provided:
const addFunc = ts.factory.createFunctionDeclaration(
/* modifiers */,
/* asteriskToken */,
/* name */,
/* typeParameters */,
/* parameters */,
/* type */,
/* body */
)
We can see that it takes a few slots for different types of AST nodes. We'll need to use a few of them, and the others we can leave blank with an undefined
value.
The first two, modifiers
and asteriskToken
we can leave blank since we don't need them here. The next one name
we can use our addId
we defined earlier. For typeParameters
we can leave blank. For parameters
, we'll need to define two, one for each parameter, using the identifiers we already created. For type
we can use the numberKeyword
we already defined, this represents the return type of the function. Finally, for body we will create a new block node that includes a return statement with a binary expression that adds the two parameters together.
There are a lot of technical terms here - for in-depth details on everything mentioned here, take a look at my new book "The Typescript Compiler API" that explains everything from A-Z for code generation, AST's and more!
Then to print it out, we can use a printer
by doing:
function print(nodes) {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const resultFile = ts.createSourceFile(
"temp.ts",
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TSX
);
console.log(printer.printList(ts.ListFormat.MultiLine, nodes, resultFile));
}
print([addFunc]);
The final script should look like this:
import ts from "typescript";
const aId = ts.factory.createIdentifier("a");
const bId = ts.factory.createIdentifier("b");
const addId = ts.factory.createIdentifier("add");
const numberKeyword = ts.factory.createKeywordTypeNode(
ts.SyntaxKind.NumberKeyword
);
const addFunc = ts.factory.createFunctionDeclaration(
undefined,
undefined,
addId,
undefined,
[
ts.factory.createParameterDeclaration(
undefined,
undefined,
aId,
undefined,
numberKeyword,
undefined
),
ts.factory.createParameterDeclaration(
undefined,
undefined,
bId,
undefined,
numberKeyword,
undefined
),
],
numberKeyword,
ts.factory.createBlock(
[
ts.factory.createReturnStatement(
ts.factory.createBinaryExpression(
aId,
ts.factory.createToken(ts.SyntaxKind.PlusToken),
bId
)
),
],
true
)
);
function print(nodes) {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const resultFile = ts.createSourceFile(
"temp.ts",
"",
ts.ScriptTarget.Latest,
false,
ts.ScriptKind.TSX
);
console.log(printer.printList(ts.ListFormat.MultiLine, nodes, resultFile));
}
print([addFunc]);
When we run node index.mjs
, we should see the following printed to the console:
function add(a: number, b: number): number {
return a + b;
}
Congratulations, you just generated your first function! If you enjoyed this exercise and want to learn more about code generation, abstract syntax trees, linters, customer diagnostics in Typescript and more, then feel free to check out my new book that covers all of this in-depth. Cheers!
Top comments (0)