Make enum and literal 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
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.
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
βοΈ Also, we want to avoid dirty lies to our compiler like as unknown as ERole.ADMIN
Solution implementation
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!
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:
Did you know?
You can easily convert enum to union of literals with the help of template literal types:
This is a quick-trick for some hot fixes.
For usual development, I would suggest to stick with the LiteralEnum
util we created today.