Are you still enumerating your types manually?!

Leveraging TypeScript Compiler API to automate the boring bits

Ján Jakub Naništa
Webtips

--

You know what? Let’s follow the literary tradition of greek epic poems, let’s skip all the introduction and jump straight into the middle of the battle, in medias res!

Have you ever seen code that looked something like this?

Or like this?

If you’re aware of the problems with the code above, feel free to skip the next couple of paragraphs. If not, let’s look at the issues with the aforementioned code.

Union type values

First, let’s dissect the ButtonType situation. In our code we defined a buttonTypes array that holds the possible values of our ButtonType union:

So far so good. Let’s now try deleting one of the buttonTypes array elements:

If you now run your code, TypeScript will not complain about the missing link value. Why would it — buttonTypes is still an array of ButtonType values, nowhere did we say it is an array of all the ButtonType values. And currently, there is no easy or pretty way of doing that. (if you are looking for an ugly hack I might have a gist for you).

We get the same problem when the powers above, represented by for example a product owner, decide we need a new ButtonType, let’s call it error:

Again, if we don’t change our original buttonTypes array, the code will still compile.

Let’s see whether there are any nice workarounds out there. If, for example, your tech lead prefers enums over unions, you might be tempted to use the fact that enum is just a fancy const:

If you now console.log the buttonTypes you might be surprised:

Y U DO THIS TYPESCRIPT?!

Well, in fact, there is a good reason — TypeScript wants you to be able to do something like this:

So it creates an object that has both the forward mapping (PRIMARY -> primary) as well as the reverse one (primary -> PRIMARY). So we are back to square one, we still need to enumerate our ButtonType manually, with the very same drawbacks as before.

Interface properties

If we now look at the second example with SomePayload, we see a similar situation. If we omit a value from our payloadKeys or add an extra key to SomePayload, our code will still compile just fine.

Now if you are as paranoid and as lazy when it comes to typing as I am, you’ll probably spend an hour or two looking for a good solution that would be less error-prone and, well, prettier. And if you are as uncompromising as me, you’ll set off to create your own solution if your search yields no results.

ts-reflection to the rescue!

Without further ado, let me introduce you to ts-reflection, a TypeScript transformer that addresses both of the problems above (and much more).

With the help of ts-reflection we can turn our ButtonType code into something like:

And our SomePayload example becomes:

If you can’t wait to try it yourself feel free to drop by the project Github or install the package from NPM. If though you want to see some advanced features, keep reading!

Going deeper

Seeing the propertiesOf function above you might have been thinking: Ohhhh I have seen this before, it’s the good ol’ ts-transformer-keys!!! Strictly speaking, ts-reflection is a superset of ts-transformer-keys: not only it gives you access to the valuesOf utility, but it also allows you to do some EVIL PROPERTY MAGIC!

Okay, that might have been an exaggeration, it’s just that I just love some drama with my coding.

propertiesOf will by default return all the public properties of a type. However, it allows you to customize its output so that you can include or exclude public, protected, private, readonly and optional properties:

It also allows you to get keys of an enum or any other type:

Thank you for reading all the way down here! If you have any comments or questions don’t hesitate to use the comments section below, if you have any ideas or feature requests please file an issue on the project Github, your input is very much appreciated!

--

--