Search code examples
typescriptenumstyping

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.


Solution

  • 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