Construct a Code Generator with TypeScript and JSON Imports
On a latest software program improvement undertaking, my group put collectively a light-weight code generator with reusable strategies that I need to share. Learn on for the why and the how.
Utilizing a Code Generator
You may want a code generator if:
- Your software program interfaces with an exterior system.
- Your software program is dependent upon the construction of data outlined in that exterior system.
You would possibly actually need a code generator if:
- That construction is more likely to change in the course of the lifetime of your software program.
- You don’t have one other great way of adapting to those adjustments over time.
We met these standards with a standard state of affairs: utilizing a third-party service to ship electronic mail.
The Context
This undertaking makes use of Postmark to ship emails. You create electronic mail templates within the Postmark internet interface:
As you’re enhancing an electronic mail template, you may reference variables, and values are anticipated to be equipped by the operate name that sends emails.
Within the instance code above, legitimate values for TemplateAlias
and TemplateModel
rely totally on edits made within the PostMark dashboard. So, how can we let our TypeScript software code know what can go right here?
The Strategy
Whereas utilizing the Postmark.js library, we seen it has strategies for locating the obtainable electronic mail templates. We may retrieve the templates from Postmark, save them to disk, and construct them into the app!
However save them as what? It wouldn’t be exhausting to jot down out some quite simple TypeScript recordsdata, however I discovered one thing even simpler: uncooked JSON. Extra on this later.
The Code
Right here’s a command-line script (extra on tsx here) that connects to PostMark, retrieves electronic mail templates, and writes them to a .JSON file on disk.
#!/usr/bin/env tsx
import fs from "fs/guarantees";
import { ServerClient } from "postmark";
async operate go() {
if (course of.argv.size != 3) {
console.log(`Utilization: pnpm tsx retrieve-email-templates.ts output.json`);
course of.exit(0);
}
const output_file = course of.argv[2];
const token = course of.env.POSTMARK_TOKEN;
if (!token) {
console.log("Please provide token through $POSTMARK_TOKEN");
course of.exit(1);
}
const consumer = new ServerClient(token);
const templates = await consumer.getTemplates({});
console.log(`Retrieved ${templates.TotalCount} templates:`);
const aliases = templates.Templates.map((t) => t.Alias).filter(isNotNull);
aliases.forEach((a) => console.log(` ${a}`));
const obj = await collectTemplates(consumer, aliases);
await fs.writeFile(output_file, JSON.stringify(obj, undefined, 2));
console.log(`nWrote ${output_file}`);
}
kind TemplateCollection = File<string, object>;
async operate collectTemplates(
consumer: ServerClient,
templateNames: string[],
): Promise<TemplateCollection> {
const [name, ...tail] = templateNames;
const mannequin = (await modelForTemplate(consumer, title)) || {};
const remaining =
tail.size == 0 ? {} : await collectTemplates(consumer, tail);
return { [name]: mannequin, ...remaining };
}
async operate modelForTemplate(consumer: ServerClient, idOrAlias: string) {
/*
There is not an effective way to ask Postmark, "what's the mannequin for this template",
however we are able to get it from the _SuggestedTemplateModel_ that comes again from validateTemplate.
*/
const instance = await consumer.getTemplate(idOrAlias);
const validated = await consumer.validateTemplate( "",
Topic: instance.Topic );
return validated.SuggestedTemplateModel;
}
export operate isNotNull<T>(enter: T | null): enter is T {
return enter !== null;
}
go();
export {};
This produces output like this:
{
"user-invitation-1": {
"title": "name_Value",
"invite_sender_name": "invite_sender_name_Value",
"invite_sender_organization_name": "invite_sender_organization_name_Value",
"product_name": "product_name_Value",
"action_url": "action_url_Value",
"support_email": "support_email_Value",
"live_chat_url": "live_chat_url_Value",
"help_url": "help_url_Value"
},
"another-email-template":{
"foo": "foo_Value"
/* ... */
}
}
Right here’s the cool half: when TypeScript imports JSON, it is aware of the construction1. We import the JSON immediately into TypeScript, derive varieties from it, and write a generic operate:
import templateJson from "./postmark-templates.gen.json";
export kind PostmarkTemplates = typeof templateJson;
export kind TemplateName = keyof typeof templateJson;
export async operate sendEmailTemplate<T extends TemplateName>(
templateName: T,
templateFields: PostmarkTemplates[T],
recipient: string,
) {
// todo: ship electronic mail
}
Now we are able to name sendEmailTemplate
with completions based mostly on the template construction from Postmark:
This helped with writing email-sending code accurately the primary time. And, extra importantly, it helps maintain the appliance code correct over time. When edits are made to the templates in Postmark, we are able to run the generator, compile the app, and comply with the squiggles!
Customized Code Generator
We’ve lengthy been followers of code turbines for issues like this. However, as with many tech choices, we hem and haw over whether or not the worth offered will recoup the upfront improvement value. It’s a simple determination when the fee is low as a consequence of requirements and tooling (like OpenAPI or GraphQL). Nonetheless, in my thoughts, a “customized code generator” was a giant hammer reserved for large issues. This undertaking modified my expectations, reducing the bar considerably, and I anticipate to make use of the approach once more. I hope it will probably prevent a while, too!
Footnotes:
1: I don’t anticipate that that is universally true throughout the wild-west ecosystem of frameworks and compilers, however it labored for this undertaking with Subsequent.js and TypeScript 5.