You might not need TypeScript enums
With Node.js learning to ignore TypeScript annotations1 and TypeScript getting an option that disallows runtime semantics2, let’s re–evaluate if we need the most commonly used TypeScript runtime features: enums.
(Spoiler: no)
Let’s look at a common example of an enum:
enum Things {
FOO = 'foo',
BAR = 'bar',
ZAR = 'zar',
}
Since values are human readable, we can use TypeScript union of strings instead:
type Thing = 'foo' | 'bar' | 'zar';
Then you can use strings for values:
let someThing: Thing = 'foo';
function logThing (thing: Thing) {
console.log(thing);
}
logThing('bar');
List values
Sometimes it’s useful to have access to all possible values at runtime. For this, we can list them in an array and use that as source of truth for types3:
const THINGS = Object.freeze(['foo', 'bar', 'zar'] as const;)
type Thing = (typeof THINGS)[number];
// ↑ this evaluates to: type Thing = "foo" | "bar" | "zar"
as const
makes TypeScript to see it as immutable (instead ofArray<string>
)Object.freeze
prevents from changing the array by accident at runtime
Now you can e.g. present user with a list of all options to choose from
(example using lit-html
):
html`<select>
${THINGS.map(thing => html`<option>${thing}</option>`)}
</select>`
… or sanitise user input:
function sanitise<Type>(
dirtyValue: any,
allowedValues: Readonly<Array<Type>>,
fallbackValue: Type | undefined
): Type {
if (allowedValues.includes(dirtyValue as Type)) {
return dirtyValue as Type;
} else {
const error = new Error(`Unrecognised value: ${dirtyValue}`);
if (fallbackValue === undefined) {
throw error;
} else {
console.warn(error);
return fallbackValue;
}
}
}
const dirtyThing = new URLSearchParams(window.location.search).get('thing');
const cleanThing = sanitise<Thing>(dirtyThing, THINGS, THINGS[0]);
Mapping
To map values to something else, use Record
to make sure all values
are included as keys (an no other keys are present):
const thingToLabelMap: Record<Thing, string> = {
foo: 'Felicity',
bar: 'Bartholomew',
zar: 'Zahary',
}
const label = thingToLabelMap[thing];
Try it out in TypeScript playground
Switch
To make sure all values are handled in a switch
statement, you can
either rely on a
EsLint switch-exhaustiveness-check rule, or
include default
case with satisfies never
:
switch (thing)
{
case 'foo':
console.debug('foo?');
break;
case 'bar':
console.debug('bar.');
break;
case 'zar':
console.debug('zar!');
break;
default:
// This will raise a TS error, if we add a new value
// without adding a case
thing satisfies never;
}
Try it out in TypeScript playground
-
--experimental-strip-types
flag introduced in Node.js 23.0.0 and no flag needed since 23.6.0 ↩ -
I’d put array definition in one location (e.g.
src/consts/…
) and types in another (e.g.src/types/…
), but you do you. ↩
Discuss this post on Mastodon.