TypeScript Enums in JavaScript? Zo gebruik je JSDoc voor Type-Safety
birds
Blogs
Martin Reurings
Blogs
02/02/2026
4 min
0

Enums in JavaScript, keep it simple, keep it typed.

02/02/2026
4 min
0

Introduction

Many people around me are well aware that I have a dislike for Typescript. I will not go into detail on why. A surprise for most is that I do like type-hinting and occasionally fully typed JavaScript. Wait, what? Right, so using JSDoc for type-hinting, which indeed runs on the typescript compiler. What follows is more-or-less the path of discovery that culminated in my now preferred solution for writing enums.

TL;DR

/**
* @enum {typeof BIRDS[keyof typeof BIRDS]}
*/

const BIRDS = /** @type {const} */ ({
DODO: 'dodo',
KIWI: 'kiwi'
})

Make certain you don’t forget to ‘cast’ your enum to const, or the above typed enum statement will be moot.

Press enter or click to view image in full size

The issue with enums

The biggest issue with enums in JavaScript is that they simply do not exist, while in TypeScript they generate compiled code and are often frowned upon. To make matters worse, people have yet to agree on how to most elegantly solve the common use-cases. I have, for myself, decided on the following structure:

const BIRDS = {
DODO: 'dodo',
KIWI: 'kiwi'
}

This allows my editor to type-hint without any additional instructions, keeping myself from making weird typos, like alert("dood") when I obviously meant to alert("dodo") by instead using alert(BIRDS.DODO). Any typo will cause any decent editor to wonder if I did not instead mean to type 'DODO'.

So, what’s the issue?

Should we take things into the realm of proper type-hinting, how do we correctly type this enum in such a way that Typescript starts telling us what's wrong. Consider the following function:

export default function shootTheBird(bird) {
console.log(`Why would you shoot the "${bird}"?`);
}

shootTheBird("penguin");

JavaScript won’t care what you’re using as input. Thanks to it’s extremely lenient nature, it’ll shoot anything you feed that function, coercion for free. However, we should be able to constrain that bird to the known values of the enum, right?

JSDoc even has a tag built-in for this purpose @enum, or so we'd think. However, remember I said we have yet to agree on the common solution for enums in JavaScript?

/**
* @enum
*/

const BIRDS = {
DODO: 'dodo',
KIWI: 'kiwi'
}

/**
* @param {BIRDS} bird - The bird you want to shoot
*/

export default function shootTheBird(bird) {
console.log(`Why would you shoot the "${bird}"?`);
}
shootTheBird("penguin");

Type-hinting fails completely:


Type-checking this with typescript results in:

> tsc -p ./tsconfig.json

src/bird.js:2:9 - error TS1110: Type expected.
2 * @enum
3 */

Not what we were looking for. The @enum tag informs us that the BIRD constant is intended as an enum without actually setting any boundaries for linting purposes.

Why doesn’t this work?

Since it did bother me that I could not use Typescript to lint this easy to spot issue out of my code, I went looking for a way to ‘correctly’ type-hint my enum. A couple of things need to come together to make this work.


It took me a while to figure out how to get a proper type-definition to work with the enum. While in JSDoc I could not use any fancy valueOf constructions that I’d found, I tried to untangle them, or perhaps entangle them, into a single line.

With some help from a colleague who’s always raving about Typescript and uses it to it’s fullest extend, the following result emerged.

/**
* @enum {typeof BIRDS[keyof typeof BIRDS]}
*/

const BIRDS = {
DODO: 'dodo',
KIWI: 'kiwi'
}

Basically a really complicated way of saying Object.values(BIRDS). Or translated to human speech; For the object of type BIRDS give me all the values for the keys of the object of type BIRDS. Yikes!

However, this would not auto-complete at all, my editor would not even realise I actually wanted a string as input. And type-checking it gave me no help either.

> tsc -p ./tsconfig.json

src/bird.js:4:7 - error TS7022: 'BIRDS' implicitly has type
'any' because it does not have a type annotation and is
referenced directly or indirectly in its own initializer.

4 const BIRDS = {
~~~~~

Solution

I admit I felt a little defeated. And at first my Typescript enthusiast colleague also didn’t see the problem. He did however come through and figured it out. In retrospect I should’ve known too, it was mentioned more than once in the various discussions I had found on Typescript solutions to, more or less, this exact problem.

Typescript needs the object to be immutable, or else it wont have any keys. I assume having a proper class will do instead, but we don’t want to duplicate our code. The answer appeared stunning to me, why would a declared const not be immutable until it is ‘cast to const’. But that’s where Object Programming clashes with type safety, although the declared const will forever be an Object, it’s members are still flexible. Casting it to {const} is merely declaring our intend, you can still mess it up on runtime…

/**
* @enum {typeof BIRDS[keyof typeof BIRDS]}
*/

const BIRDS = /** @type {const} */ ({
DODO: 'dodo',
KIWI: 'kiwi'
})

Now we’ve confirmed this object is an enum while further specifying that it consists of two valid string-values. There's a weird duality in this configuration where defining something as being @type {BIRDS} will evaluate to one of two string, yet using the BIRDS instance will still happily code-hint it's two keys too, for instance BIRDS.DODO.


Luckily, although I find it strange behaviour, it’s exactly what I want. I can use the enum to keep myself from making silly coding mistakes and have TypeScript lint incorrect values (or hint the correct strings, removing the need to import the enum).

> tsc -p ./tsconfig.json

src/bird.js:16:14 - error TS2345: Argument of type
'"penguin"' is not assignable to parameter of type 'BIRDS'.
16 shootTheBird("penguin");
~~~~~~~~~

So, what about Typescript?

Finally a little side-note about Typescript. After all, I did mention that the real enum should not be used… I personally like to keep my solution as close to vanilla as possible, which results in the following syntax.

export const MAMMELS = {
DOG: 'dog',
LEOPART: 'leopart'
} as const;

export default MAMMELS;
export type MAMMEL_KEY = keyof typeof MAMMELS;
export type MAMMEL = (typeof MAMMELS)[MAMMEL_KEY];

Since I sometimes pass along the enum’s key, and other times working with the value, this way I always have them readily available. I sure won’t mind if some typescript guru can tell me how to achieve this end-result with a more reusable syntax?

Reacties
Categorieën