Alex Pappas
Alex Pappas

Reputation: 2598

How to properly type an array of enum entries?

I have a basic enum below:

export enum Fruit {
   apple,
   banana
}

I want to export a fixed Array of the enum keys

export const Fruits = Object.entries(Fruit).map(f => f[0]);

Which should give, as desired, ['apple', 'banana'] and Fruits typed as string[].

In an attempt for a more specific typing, I added as keyof typeof Fruit like so

 export const Fruits = Object.entries(Fruit).map(f => f[0] as keyof typeof Fruit);

Which gives me the type const Fruits: ("apple" | "banana")[]. Is this the most that I can get?

I was aiming to get a typing like const Fruits: ["apple", "banana"] = .... which I think is the perfect typing I should produce.

Footnote:

I wouldn't like to utilize the other method of defining enums, Just for the sake of avoiding that redundancy,

export enum Fruit {
   apple = 'apple',
   banana = 'banana'
}

And I am happy to do something like:

interface Meal {
   fruit: keyof tpeof Fruit // since the default enum values are integers, use keys
}

So I'd be happy to have a solution that doesn't require me to do so. If there's no other way, do mention in your answer.

Upvotes: 1

Views: 804

Answers (1)

Please keep in mind that there is no guarantee that Object.entries or Object.keys preserve the order of keys. Hence you need to return a permutation of all possible states and not only ['apple', 'banana'].

In this case it should be ['apple', 'banana'] | ['banana', 'apple'].

export enum Fruit {
    apple,
    banana
}

// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends PropertyKey, R extends any[] = []> = {
    [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];

const keys = <
    Keys extends string,
    Obj extends Record<Keys, unknown>
>(obj: Obj) =>
    Object.keys(Fruit) as TupleUnion<keyof Obj>;

const result = keys(Fruit)

// ["apple", "banana"] | ["banana", "apple"]
type Check = typeof result

Playground I have used keys instead of entries because we are interested only in keys.

Here, in my blog, you can find more about convertiong union to tuples and function arguments inference

So, which enum is better: with values as integers or strings?

First of all, enums has their own flaws. COnsider this example:

export enum Fruit {
    apple,
    banana
}
const fruit = (enm: Fruit) => {}

fruit(100) // ok, no error

Is it safe - no!

Enums with integers are got to use if you have a bit mask.

It is better to use enum with string values:

export enum Fruit {
   apple = 'apple',
   banana = 'banana'
}

If you still want to use enum with integers, consider this example:

const enum Fruit {
    apple,
    banana,
}

const fruit = (enm: typeof Fruit) => { }

fruit(100) // expected error

Object.keys(Fruit) // impossible

If you want to use enum with integers and Object.keys/entries you might want to use a safest approach:

export const Fruit = {
    apple: 0,
    banana: 1,
} as const

const fruit = (enm: typeof Fruit) => { }

fruit(100) // expected safe

Upvotes: 2

Related Questions