Are you still enumerating your types manually?!
Leveraging TypeScript Compiler API to automate the boring bits
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!