We have a TypeScript package that uses and exports 1 this enum:

export const enum AudioFormat {
    STEREO,
    SURROUND_5_1,
    SURROUND_7_1,
    ATMOS,
}

And we’d like to make available to consumers of our package an array containing all the values of this enum. We could do the following:

export const ALL_AUDIO_FORMATS = Object.values(AudioFormat);

But this has several problems.

The first one is that it doesn’t actually work. Since we’re using a const enum (instead of a regular enum) we get the following error:

‘const’ enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.

But let’s say we don’t care about the inlining of const enums in our package.2 Let’s say we use a regular enum. In that case Object.values does work but:

  1. We’re not actually exporting an array. Consumers of our package are bundling code that at runtime creates an array from the AudioFormat object and then exports that.

  2. This calculation at runtime happens for every user that visits a website that bundles our package. Seems kind of wasteful to be calculating the same thing over and over again.

  3. Object.values does not exist on older devices so now consumers of our package may need to polyfill it.

All these disadvantages make this a no-go. The ideal solution would be to export an actual array containing all enum values. Basically this:

export const ALL_AUDIO_FORMATS = [0, 1, 2, 3];

To achieve this we could evaluate the TypeScript enum at build time and emit into the final JavaScript bundle this array.3 But this seems somewhat complex and we would likely need to add another dependency to our build toolchain.

There should be a way to use TypeScript types to guarantee that an array contains all the values of an enum. A naive approach would be the following:

export const ALL_AUDIO_FORMATS: Array<AudioFormat> = [0, 1, 2, 3];

But this doesn’t actually do what we want. Array<AudioFormat> ensures that ALL_AUDIO_FORMATS only contains AudioFormats but it doesn’t ensure that it contains all possible AudioFormats. TypeScript would accept this:

export const ALL_AUDIO_FORMATS: Array<AudioFormat> = [1, 1, 1];

The code would also still compile if someone added a new value to our enum, but we want compilation to fail in that situation.

What can we do then? Some TypeScript magic:

type UnionToIntersection<U> = (U extends never ? never : (arg: U) => never) extends (arg: infer I) => void ? I : never;
type UnionToTuple<T> = UnionToIntersection<T extends never ? never : (t: T) => T> extends (_: never) => infer W
    ? [...UnionToTuple<Exclude<T, W>>, W]
    : [];

Some recursion 4, plus the spread operator and infer and presto! This now works as expected:

export const ALL_AUDIO_FORMATS: UnionToTuple<AudioFormat> = [
    AudioFormat.STEREO,
    AudioFormat.SURROUND_5_1,
    AudioFormat.SURROUND_7_1,
    AudioFormat.ATMOS,
];

Removing any of these values from the array results in a compile error. Adding a new value to the enum without updating the array also results in a compile error.

This solution does require some manual work to keep the array up-to-date with the enum. But it’s simple enough for most use-cases. Give it a try!

  1. To be able to export a const enum we need to use the tsconfig option preserveConstEnums

  2. We should prefer const enum to enum. Generally bundle size is reduced and runtime performance is improved when using const enum

  3. There are already some solutions for evaluating JavaScript at build time such as Preval and Prepack but I have not used any of them so I can’t vouch for them. 

  4. Do note that older versions of TypeScript do not support recursive type aliases and throw the error:

    Type alias ‘UnionToTuple’ circularly references itself.

    For the magic to work we must use at least TypeScript v4.1.5.