How to generate a union type using ts-morph
I was hacking on some dev tooling at work, and in an attempt to introduce type-safety and auto-complete convenience for the tool I was building, I wanted to generate a union type from a bunch of strings.
Suppose you have an array of strings:
const reasons = ["tech-debt", "arbitrary-deadlines", "unclear-expectations"];
and you wanted to generate this TypeScript union type from it:
type FailureReason = "tech-debt" | "arbitrary-deadlines" | "unclear-expectations";
You could just do the following:
const contents = `type FailureReason = ${reasons.map(wrapInQuotes).join(" | ")};`
fs.writeFile(someFile, contents);
This gets the job done, but not guaranteed to be syntactically correct TypeScript. It's probably fine, until it isn't.
If we're being serious about generating code, then we want to manipulate the AST,
which allows us to reason about code within the correct domain: code is not a string
.
let's do it properly
A good library for this kind of thing is ts-morph. We're going to use the
addTypeAlias
function, combined with the curiously-undocumented
Writers.unionType
.
The documentation does not mention this, but the type
field passed into addTypeAlias
also accepts a function of type:
type WriterFunciton = (writer: CodeBlockWriter) => void;
which just happens to be what Writers.unionType
returns. Sweet.
Writers.unionType
also requires at least 2 arguments — after all, what is a union with fewer than 2 members?
(btw, join a union.) So, we will have to cover the cases where our source array has a length of 0 or 1: an early return
for the former, and aliasing to just that specific value for the latter.
So combining everything, the code roughly looks like this:
import { Project, Writers } from 'ts-morph';
const reasons: string[] = getAllPossibleReasonsFromSomewhere();
if (reasons.length === 0) {
return;
}
const aliasedType = reasons.length === 1
? reasons[0]
: Writers.unionType(
reasons[0],
reasons[1],
...reasons.slice(2)
);
const project = new Project();
const output = project.createSourceFile('./failureReason.ts');
output.addTypeAlias({
name: 'FailureReason',
type: aliasedType,
})
file.save();