Another approach is to wrap the enum value inside of a class. Then we can apply the decorator to the wrapper class. This is different from the "Enum Class" alternative of this answer, because it keeps the native enum.
The advantages are obvious, e. g. better intellisense, type safety and compatibility with API that expect native enum.
A disadvantage is more boilerplate code for declaring and using the enum (e. g. you have to write new Response(value) to initialize the wrapped enum).
Declaration of enum:
enum ResponseEnum {
No = 0,
Yes = 1,
}
@EnumDescriptor( ResponseEnum, {
No = "this is No",
Yes = "this is Yes",
// Instead of strings, one could assign arbitrary metadata:
// Yes = { answer: "this is Yes", more: 42 }
})
class Response {
constructor( public value: ResponseEnum = ResponseEnum.No ){}
}
The above code is typesafe which is achived through the typing of the decorator function (see below). If we miss an enum key in the descriptor or if there is a mismatch between the native enum type passed to @EnumDescriptor and the one used by the wrapper class, the TS compiler will raise an error.
Instanciate enum and get metadata:
let r = new Response( ResponseEnum.Yes );
console.log("---VALUE---");
console.log( r );
console.log( +r ); // conversion to primitive, same as r.valueof()
console.log( r.valueOf() );
console.log( r.toString() );
console.log( JSON.stringify( r ) );
console.log("---METADATA---");
// Get metadata from variable
console.log( Reflect.getMetadata( EnumValuesMetadataKey, Object.getPrototypeOf( r ) ) );
console.log( Reflect.getMetadata( EnumDescriptorMetadataKey, Object.getPrototypeOf( r ) ) );
// Get metadata from the wrapper class:
console.log( Reflect.getMetadata( EnumValuesMetadataKey, Response.prototype ) );
console.log( Reflect.getMetadata( EnumDescriptorMetadataKey, Response.prototype ) );
Console output:
---VALUE---
ResponseĀ {value: 1}
1
1
1
1
---METADATA---
{0: "No", 1: "Yes", No: 0, Yes: 1}
{No: "this is No", Yes: "this is Yes"}
{0: "No", 1: "Yes", No: 0, Yes: 1}
{No: "this is No", Yes: "this is Yes"}
Implementation of decorator function (library code):
Though not originally requested by OP, the decorator also stores meta data about the original enum type via EnumValuesMetadataKey (mapping of keys to values). We already have this information and it is very important for use cases like an object editor, where we want to know at runtime, which enum keys are available for a given enum member.
Also, standard methods of Object.prototype are overridden to "unwrap" the native enum value, namely conversion to primitive value (.valueOf()), to string (.toString()) and to JSON (.toJSON()). This is similar to what build-in wrapper classes like Number do.
export interface IEnumWrapper< EnumT > {
value: EnumT;
}
type EnumWrapperCtor< EnumT > = new( value: EnumT ) => IEnumWrapper< EnumT >;
export const EnumValuesMetadataKey = Symbol("EnumValuesMetadataKey");
export const EnumDescriptorMetadataKey = Symbol("EnumDescriptorMetadataKey");
export function EnumDescriptor< EnumT, KeysT extends string >(
enumValues: { [ key in KeysT ]: EnumT },
descriptor: { [ key in KeysT ]: any })
{
return function( target: EnumWrapperCtor< EnumT > ) {
// Assign metadata to prototype of wrapper class
Reflect.defineMetadata( EnumValuesMetadataKey, enumValues, target.prototype );
Reflect.defineMetadata( EnumDescriptorMetadataKey, descriptor, target.prototype);
// Override standard methods to "unwrap" the enum value
target.prototype.valueOf = function() { return this.value };
target.prototype.toJSON = function() { return this.value; }
target.prototype.toString = function() { return this.value.toString() };
}
}