Are you still enumerating your types manually?!

janjakubnanista

Ján Jakub Naništa

Posted on June 25, 2020

Are you still enumerating your types manually?!

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?

// You have some sort of a literal type
type ButtonType = 'primary' | 'secondary' | 'link';

// And then somewhere else you want to list all its values,
// maybe in a dropdown in your storybook or a WYSIWYG editor
const buttonTypes: ButtonType[] = ['primary', 'secondary', 'link'];
Enter fullscreen mode Exit fullscreen mode

Or like this?

// You have some sort of an interface, API request payload for example
interface SomePayload {
  name: string;
  quantity: number;
}

// Then somewhere in your codebase you want to do something
// with the properties of this interface
const payloadKeys = ['name', 'quantity'];
Enter fullscreen mode Exit fullscreen mode

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 disect the ButtonType situation. In our code we defined a buttonTypes array that holds the possible values of our ButtonType union:

const buttonTypes: ButtonType[] = ['primary', 'secondary', 'link'];
Enter fullscreen mode Exit fullscreen mode

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

// ButtonType 'link' is no longer in the array
const buttonTypes: ButtonType[] = ['primary', 'secondary'];
Enter fullscreen mode Exit fullscreen mode

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:

//                          Our new error type ↴
type ButtonType = 'primary' | 'secondary' | 'error' | 'link';
Enter fullscreen mode Exit fullscreen mode

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

// This compiles fine even though we now have a new ButtonType
const buttonTypes: ButtonType[] = ['primary', 'secondary', 'link'];
Enter fullscreen mode Exit fullscreen mode

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:

enum ButtonType {
  PRIMARY = 'primary',
  SECONDARY = 'secondary',
  LINK = 'link'
}

const buttonTypes: ButtonType[] = Object.values(ButtonType) as ButtonType[];
Enter fullscreen mode Exit fullscreen mode

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

console.log(buttonTypes);

// The output does not look like what we'd expect!
['primary', 'PRIMARY', 'secondary', 'SECONDARY', 'link', 'LINK']
Enter fullscreen mode Exit fullscreen mode

Y U DO THIS TYPESCRIPT?!

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

const enumKey = ButtonType[ButtonType.PRIMARY]
Enter fullscreen mode Exit fullscreen mode

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:

import { valuesOf } from 'ts-reflection';

// OMG SO MUCH BETTER
const buttonTypes: ButtonType[] = valuesOf<ButtonType>();
Enter fullscreen mode Exit fullscreen mode

And our SomePayload example becomes:

import { propertiesOf } from 'ts-reflection';

// FREEDOM!!!
const payloadKeys = propertiesOf<SomePayload>();
Enter fullscreen mode Exit fullscreen mode

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, 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:

// This will list all the readonly properties of MyInterface
const readonlyProperties = propertiesOf<MyInterface>({ readonly: true });

// This will list all the optional properties of MyInterface
const optionalProperties = propertiesOf<MyInterface>({ optional: true });

// This will list all the required properties of MyInterface
const requiredProperties = propertiesOf<MyInterface>({ optional: false });

// But feel free to ask for private readonly OR public optional properties
const verySpecificProperties = propertiesOf<MyInterface>(
  { private: true, readonly: true }
  { public: true, optional: true }
);

// Or maybe a combination of required non-public properties
// and protected optional ones? I mean why not
const evenMoreSpecificProperties = propertiesOf<MyInterface>(
  { public: false, optional: false }
  { protected: true, optional: true }
);
Enter fullscreen mode Exit fullscreen mode

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

const buttonTypeKeys = propertiesOf<typeof MyEnum>();
const stringProperties = propertiesOf<string>();
const promiseProperties = propertiesOf<Promise<unknown>>();
Enter fullscreen mode Exit fullscreen mode

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 verimuch appreciated!

💖 💪 🙅 🚩
janjakubnanista
Ján Jakub Naništa

Posted on June 25, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related