Make enum and literal compatible

Make enum and literal compatible
Make enum and literal types compatible

In TypeScript, enum can be represented in multiple forms:

  • Numeric enums ("default")
  • String enums (where key represents a literal string)
  • Heterogeneous enums (mix of numbers and strings, some of them are computed in runtime)

Among these multiple forms I believe string enums are the most used in web apps development because of their human readability. Unfortunately, string enums are not "default" version of enums in TypeScript , so whenever you need to use enums in a human readable form you need to explicitly define values for every member of enum. This comes with an awkward incompatibility between enums as described below.

Issue demo

enum ERole {
    USER = 'USER',
    ADMIN = 'ADMIN',
}

enum ERoleBeta {
    USER = 'USER',
    ADMIN = 'ADMIN',
}

// πŸ€¦πŸ»β€β™‚οΈ nightmare πŸ€¦πŸ»β€β™‚οΈ
const role: ERole.ADMIN = ERoleBeta.ADMIN;
// ❌ ^ Type 'ERoleBeta.ADMIN' is not assignable to type 'ERole.ADMIN'.(2322)
different enums are incompatible

Code above in runtime (in JavaScript) will be equivalent to const role = 'USER' and we know TypeScript marked variable role as of type ERole.ADMIN which value is literal string 'USER'. Nevertheless, TypeScript compiler does not allow such assignment because different enums are not compatible even if their values are the same.

// πŸ€¦πŸ»β€β™‚οΈ nightmare πŸ€¦πŸ»β€β™‚οΈ
const role: ERole.ADMIN = 'ADMIN';
// ❌ ^ Type '"ADMIN"' is not assignable to type 'ERole.ADMIN'.(2322)
literal string is not assignable to string enum

Also, you cannot assign literal string to enum type.

All these restrictions make sense if we would use number enums, or mixed enums. But if we conventionally agree enums to be only string enums there should be a better way to write our code!

Solution goals

let assignableToEnum: EBaseRole;
// βœ… compatible with self values
assignableToEnum = EBaseRole.USER;
// βœ… compatible with literals
assignableToEnum = 'USER';
// βœ… compatible with other enums for same values
assignableToEnum = EAdvancedRole.USER;

let assignableToLiteral: 'USER';
// βœ… compatible with all enums same values
assignableToLiteral = EBaseRole.USER;
assignableToLiteral = EAdvancedRole.USER;

// βœ… error raised at compile time
let expectError: EBaseRole = EAdvancedRole.VIEWER;
//  ^ Type '"VIEWER"' is not assignable to type 'EBaseRole'.(2322)
make enum and literals compatible

☝️ Also, we want to avoid dirty lies to our compiler like as unknown as ERole.ADMIN

Solution implementation

const LiteralEnum = <T extends string>(...values: T[]): {
    [K in T]: K;
} => {
    const res = {} as any;
    values.forEach(v => {
        res[v] = v;
    })
    return res;
}
LiteralEnum util to build string enums

Create util function to build our enums from literals arguments:

  • To make sure our keys equal to values
  • To emphasise the way we create enums
  • To easily search through a project which enums were created by our util

P.S. I decided to call the util with capitalised character to highlight its specialty. We try to workaround native language inconveniences.

The important trick!

export const EBaseRole = LiteralEnum(
    'USER',
    'ADMIN',
);
export type EBaseRole = keyof typeof EBaseRole;
present enum as union of literals

Now, when we create our enum, we define type of same name as the const to leverage declaration merging technique of TypeScript.

Now, TypeScript compiler can smartly recognise context and use either type of enum or enum value 🧠 πŸŽ‰

Full working demo

const LiteralEnum = <T extends string>(...values: T[]): {
    [K in T]: K;
} => {
    const res = {} as any;
    values.forEach(v => {
        res[v] = v;
    })
    return res;
}

export const EBaseRole = LiteralEnum(
    'USER',
    'ADMIN',
);
// βœ… easy to convert to union
export type EBaseRole = keyof typeof EBaseRole;

export const EAdvancedRole = LiteralEnum(
    'USER',
    'ADMIN',
    'VIEWER',
);
export type EAdvancedRole = keyof typeof EAdvancedRole;

let assignableToEnum: EBaseRole;
// βœ… compatible with self values
assignableToEnum = EBaseRole.USER;
// βœ… compatible with literals
assignableToEnum = 'USER';
// βœ… compatible with other enums for same values
assignableToEnum = EAdvancedRole.USER;

let assignableToLiteral: 'USER';
// βœ… compatible with all enums same values
assignableToLiteral = EBaseRole.USER;
assignableToLiteral = EAdvancedRole.USER;

// βœ… error raised at compile time
let expectError: EBaseRole = EAdvancedRole.VIEWER;
//  ^ Type '"VIEWER"' is not assignable to type 'EBaseRole'.(2322)

You can try this in TypeScript playground:

TS Playground - An online editor for exploring TypeScript and JavaScript
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.

Did you know?

You can easily convert enum to union of literals with the help of template literal types:

enum ERole {
    USER = 'USER',
    ADMIN = 'ADMIN',
}

const unionFromEnum: `${ERole}`;
//    ^ unionFromEnum: "USER" | "ADMIN"

// βœ… 
unionFromEnum = 'USER';
// βœ… 
unionFromEnum = ERole.USER;
interpolate enum to union of literals

This is a quick-trick for some hot fixes.

For usual development, I would suggest to stick with the LiteralEnum util we created today.