How to use decorators on enum in TypeScript

You can't. Where you can use decorators in TypeScript:

Class Decorators

@sealed
class Greeter {}

Method Decorators

class Greeter {
    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

Accessor Decorators

class Point {
    private _x: number;

    @configurable(false)
    get x() { return this._x; }
}

Property Decorators

class Greeter {
    @format("Hello, %s")
    greeting: string;
}

Parameter Decorators

class Greeter {
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

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() };
    }
}

Short answer is, you can't (as of this writing). There are some alternatives though.

Alternative: Doc Comments

If you only want to add descriptions to your enum literals, you could use doc comments.

enum Response {
    /**
     * this is No
     */
    No = 0,
    /**
     * this is Yes
     */
    Yes = 1,
}

While the descriptions won't be available at runtime, they will show up in editor auto-completion:

auto-completion example

Alternative: Enum Class

If you really, really need the decorated info on the literals at runtime, you could use a class instead. Since decorators can be applied to class properties, you can write a class, decorate its properties and then use an instance of the class as your "enum".

function Descriptor(description: string) { 
    return (target: any, propertyName: string) => {
        // process metadata ...        
    };
}

class ResponsesEnum {
    @Descriptor("this is Yes")
    readonly Yes = 1;
    @Descriptor("this is No")
    readonly No = 2;
}
const Responses = new ResponsesEnum();

Try it here.

Tags:

Typescript