Are you still enumerating your types manually?!
Tags: typescript, javascript, transformer, reflection
Published: 2020-06-25 15:25:46

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_](https://en.wikipedia.org/wiki/In_medias_res)!

Have you ever seen code that looked something like this?

```typescript
// 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'];
```

Or like this?

```typescript
// 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'];
```

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:

```typescript
const buttonTypes: ButtonType[] = ['primary', 'secondary', 'link'];
```

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

```typescript
// ButtonType 'link' is no longer in the array
const buttonTypes: ButtonType[] = ['primary', 'secondary'];
```

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](https://gist.github.com/janjakubnanista/7cc75df9a2b26277f443da8048fbbdf9)).

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

```typescript
//                          Our new error type ↴
type ButtonType = 'primary' | 'secondary' | 'error' | 'link';
```

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

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

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

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

const buttonTypes: ButtonType[] = Object.values(ButtonType) as ButtonType[];
```

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

```typescript
console.log(buttonTypes);

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

**Y U DO THIS TYPESCRIPT?!** 

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

```typescript
const enumKey = ButtonType[ButtonType.PRIMARY]
```

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:

```typescript
import { valuesOf } from 'ts-reflection';

// OMG SO MUCH BETTER
const buttonTypes: ButtonType[] = valuesOf<ButtonType>();
```

And our `SomePayload` example becomes:

```typescript
import { propertiesOf } from 'ts-reflection';

// FREEDOM!!!
const payloadKeys = propertiesOf<SomePayload>();
```

If you can't wait to try it yourself feel free to drop by the [project Github](https://github.com/janjakubnanista/ts-reflection) or [install the package from NPM](https://www.npmjs.com/package/ts-reflection). 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`](https://www.npmjs.com/package/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:

```typescript
// 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 }
);
```

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

```typescript
const buttonTypeKeys = propertiesOf<typeof MyEnum>();
const stringProperties = propertiesOf<string>();
const promiseProperties = propertiesOf<Promise<unknown>>();
```

---

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](https://github.com/janjakubnanista/ts-reflection), your input is verimuch appreciated!