Skip to content

Commit

Permalink
Add "Enums considered harmful"
Browse files Browse the repository at this point in the history
  • Loading branch information
smack0007 authored Jan 24, 2023
1 parent 6df3572 commit 11f839f
Showing 1 changed file with 105 additions and 0 deletions.
105 changes: 105 additions & 0 deletions src/blog/2023/enums-considered-harmful.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
Title: Enums considered harmful
Subtitle:
Date: 2023-01-24
Tags: typescript, enum
---

It seems that torwards the end of 2022 the collective hivemind of the TypeScript
programming world decided that [enums](https://www.youtube.com/watch?v=jjMbPt_H3RQ)
are [terrible](https://www.youtube.com/watch?v=0fTdCSH_QEU). While I mostly agree
with the premise that TypeScript enums are not good the solutions that are
presented often only deal with [String enums](https://www.typescriptlang.org/docs/handbook/enums.html#string-enums).
For my work on [SDL_ts](https://github.com/smack0007/SDL_ts)
I really need a solution for [Numeric enums](https://www.typescriptlang.org/docs/handbook/enums.html#numeric-enums)
as [SDL](http://www.libsdl.org/) is full of them.

<!--more-->

Uses for numeric enums really boil down to two use cases: the value must be either exactly
one of the specified values or some bitwise combination of them. We'll create the type helpers
`Enum` and `Flags` to implement the two use cases.

`Enum` is when the value must be one of the specified values and only one of the specified values and is easy enough to implement:

```ts
export type Enum<T> = T[keyof T];

export const MyEnum = {
// ^? const MyEnum: { readonly One: 1; readonly Two: 2; }
One: 1,
Two: 2
} as const;

export type MyEnum = Enum<typeof MyEnum>;
// ^? type MyEnum = 1 | 2

// This works.
const foo: MyEnum = MyEnum.Two;

// @ts-expect-error This does not.
const bar: MyEnum = 3;
```

> [TS Playground Link](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcCiA7ArgWwDwAqAfHALxwEDaA1sIhAGYUC6A3ALABQXokscAYwgYAzvACyiTLjJwA3lwD0iuKrXq4APQD8XVQHkMwAFxwAjABo9FAO4RTAJi4BfOAEMRg4WI7c-vaHgkFDhJaRxZcLxg4EZQqWwcIl9lDQ0dLkzOVIIACwBLTzsoahEAOi4hUXgGCHt48NkwxLKCO18lFQABGBEAWl5gARgBqChoCgLPABMIYE8MCBgKziqxOAAjNyhTZplyAGZfIA)
The type helper `Enum` just creates a union of all the values of the object. `T` could
also be more restrictive but it doesn't have to be for our purposes.

To use the helper we create the `export const MyEnum` first to hold the values
and mark it `as const`. This changes the type of `MyEnum` from
`const MyEnum: { One: number; Two: number; }` to
`const MyEnum: { readonly One: 1; readonly Two: 2; }`. Then comes the `export type MyEnum`
which passes `typeof MyEnum` to our `Enum` helper. The `typeof MyEnum` tells TypeScript that
we're talking about the `const` that we defined beforehand. The resulting type is a union
of `1 | 2`.

`Flags` is a bit more complicated:

```ts
declare const _: unique symbol;
export type Flags<T, Name> =
| {
[K in keyof T]: { [_]: Name } & T[K];
}[keyof T]
| number;

export const MyFlags = {
// ^? const MyFlags: { readonly One: 1; readonly Two: 2; }
One: 1,
Two: 2
} as const;


export type MyFlags = Flags<typeof MyFlags, "MyFlags">;
// ^? type MyFlags = number | ({ [_]: "MyFlags"; } & 1) | ({ [_]: "MyFlags"; } & 2)

// This works.
const foo: MyFlags = MyFlags.Two;

// This also works.
const bar: MyFlags = MyFlags.One | MyFlags .Two;

// Unfortunately this also works though.
const baz: MyFlags = 4;
```

> [TS Playground Link](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEYD2A7AzgF3gfQFzwFcUBLARwITQE8BbAIyQgG4BYAKBAA8AHJGLDFW4IAYtADmaADwAVADTwAclBogAfPAC87ePAA+8AN47d8ANoBpeMRTwA1iCpIAZvBkBdfIfPZPSlQgAvvAAZG6W7qxsuoFmDk6uHiYGKAT0IDBR7Fy8-IiomPAAslRiUJJaRuwA9NWm9fUAegD8JgDyKCD4AIxyJjIA7kj4AEzswVBo+egYWWzZPHwCQgglZRWa8OvSgsIuxaUSaAoARGtHJ2pRtQ0NLew1dTIAFsRTQzB2aAB07Mgz8GcSGGB22lXO5R+gyQcxuLze8CgEDQSHgHy+vzY-0KdFg+AhG1BR2+HQQBgJU2+0NhdQAqiggfwiFAMCAIFR4BhXlMkSi0XwvpznkgCOJnpjsVhcQAvfGHSGVAAsUSAA)
The `Flags` type helper looks quite [gnarly](https://www.youtube.com/watch?v=bVfopcz4nas) but
let's go through it step by step. First `declare const _: unique symbol;` just creates a
[Symbol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
that we can use at the type level. Then first part of the union
(`{ [K in keyof T]: { [_]: Name } & T[K]; }[keyof T]`) in the type helper can be thought of
like calling [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) on the type and performing what is known as
[branding or nominal typing](https://basarat.gitbook.io/typescript/main-1/nominaltyping#nominal-typing)
on each value of the enum using the string provided in the 2nd parameter of the type helper.
The last part of the helper just unions the newly created type together with `number`.

Using `Flags` is almost the same as using `Enum` but we'll have to provide a string for the
branding which I just set to be the same as the name of the enum.

Why do the enum values have to be branded? Without the branding TypeScript
widens the resulting type to just be `number`. The examples still work as before
but the Intellisense is completely lost. The IDE will just show the type `number` with
no mention of the name of the enum. See this [playground link](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcBiAbAhgcwM4A8AKgHxwC8cRA2gNbCIQBmVAunAD5wB2ArgLYAjYFADcAWABQU0JFhwAxhG554AWUSZceCnADeUgPSG4ps+bgA9APxTTAeW7AAXHACMAGjtUA7hFcATFIAvnBYOkoqMBLSsbLQ8EgocBpa+LpphEnAzCma2PgkMcYWFjZSRiZEABYAljp+ULR4AHRSkapwTBD+eZm6qQWtRH4xlVR1OlgYeBBwjc1tkh3wglhQroPaA-naLY6oXFvpLSMQY5IlAKrc3bC83FgwwBiICJNhM3MLOjDVELwcNUlis4GsAF6bXbpSgAFlEQA)

0 comments on commit 11f839f

Please sign in to comment.