'Add functions to an Enum

Is it possible to add functions to an Enum type in TypeScript?

for example:

enum Mode {
    landscape,
    portrait,

    // the dream...
    toString() { console.log(this); } 
}

Or:

class ModeExtension {
    public toString = () => console.log(this);
}

enum Mode extends ModeExtension {
    landscape,
    portrait,
}

Of course the toString() function would contain something like a switch But a use-case would flow along the lines of:

class Device {
    constructor(public mode:Mode) {
        console.log(this.mode.toString());
    }
}

I understand why extending an enum might be a strange thing, just wondering if it is possible.



Solution 1:[1]

You can either have a class that is separate to the Enum and use it to get things you want, or you can merge a namespace into the Enum and get it all in what looks like the same place.

Mode Utility Class

So this isn't exactly what you are after, but this allows you to encapsulate the "Mode to string" behaviour using a static method.

class ModeUtil {
    public static toString(mode: Mode) {
        return Mode[mode];
    }
}

You can use it like this:

const mode = Mode.portrait;
const x = ModeUtil.toString(mode);
console.log(x);

Mode Enum/Namespace Merge

You can merge a namespace with the Enum in order to create what looks like an Enum with additional methods:

enum Mode {
    X,
    Y
}

namespace Mode {
    export function toString(mode: Mode): string {
        return Mode[mode];
    }

    export function parse(mode: string): Mode {
        return Mode[mode];
    }
}

const mode = Mode.X;

const str = Mode.toString(mode);
alert(str);

const m = Mode.parse(str);
alert(m);

Solution 2:[2]

You can get the string value of an non-const enum by using square brackets:

class Device {
    constructor(public mode:Mode) {
        console.log(Mode[this.mode]);
    }
}

You can also put some enum-specific util functions into the enum, but that's just like static class members:

enum Mode {
    landscape,
    portrait
}

namespace Mode {
    export function doSomething(mode:Mode) {
        // your code here
    }
}

Solution 3:[3]

Convert your enum to the enum pattern. I find this is a better practice in general for many languages, since otherwise you restrict encapsulation options for your type. The tendency is to switch on the enum value, when really any data or functionality that is dependent on the particular enum value should just go into each instance of the enum. I've added some more code to demonstrate.

This might not work if you are particularly dependent on the underlying enum values. In that case you would need to add a member for the old values and convert places that need it to use the new property.

class Mode {
   public static landscape = new Mode(1920, 1080);
   public static portrait = new Mode(1080, 1920);

   public get Width(): number { return this.mWidth; }
   public get Height(): number { return this.mHeight; }

   // private constructor if possible in a future version of TS
   constructor(
      private mWidth: number,
      private mHeight: number
   ) {
   }

   public GetAspectRatio() {
      return this.mWidth / this.mHeight;
   }
}

Solution 4:[4]

An addition to Fenton's solution. If you want to use this enumerator in another class, you need to export both the enum and the namespace. It would look like this:

export enum Mode {
    landscape,
    portrait
}

export namespace Mode {
    export function toString(mode: Mode): string {
        return Mode[mode];
    }
}

Then you just import the mode.enum.ts file in your class and use it.

Solution 5:[5]

can make enum like by private constructor and static get return object

export class HomeSlideEnum{

  public static get friendList(): HomeSlideEnum {

    return new HomeSlideEnum(0, "friendList");

  }
  public static getByOrdinal(ordinal){
    switch(ordinal){
      case 0:

        return HomeSlideEnum.friendList;
    }
  }

  public ordinal:number;
  public key:string;
  private constructor(ordinal, key){

    this.ordinal = ordinal;
    this.key = key;
  }


  public getTitle(){
    switch(this.ordinal){
      case 0:

        return "Friend List"
      default :
        return "DChat"
    }
  }

}

then later can use like this

HomeSlideEnum.friendList.getTitle();

Solution 6:[6]

ExtendedEnum Class

I always loved the associated types in Swift and I was looking to extend Typescript's enum basic functionality. The idea behind this approach was to keep Typescript enums while we add more properties to an enum entry. The proposed class intends to associate an object with an enum entry while the basic enum structure stays the same.

If you are looking for a way to keep vanilla Typescript enums while you can add more properties to each entry, this approach might be helpful.

Input

enum testClass {
    foo = "bar",
    anotherFooBar = "barbarbar"
}

Output

{
  entries: [
    {
      title: 'Title for Foo',
      description: 'A simple description for entry foo...',
      key: 'foo',
      value: 'bar'
    },
    {
      title: 'anotherFooBar',
      description: 'Title here falls back to the key which is: anotherFooBar.',
      key: 'anotherFooBar',
      value: 'barbarbar'
    }
  ]
}

Implementation

export class ExtendedEnum {
    entries: ExtendedEnumEntry[] = []

    /**
     * Creates an instance of ExtendedEnum based on the given enum class and associated descriptors.
     * 
     * @static
     * @template T
     * @param {T} enumCls
     * @param {{ [key in keyof T]?: EnumEntryDescriptor }} descriptor
     * @return {*}  {ExtendedEnum}
     * @memberof ExtendedEnum
     */
    static from<T extends Object>(enumCls: T, descriptor: { [key in keyof T]?: EnumEntryDescriptor }): ExtendedEnum {
        const result = new ExtendedEnum()
        for (const anEnumKey of Object.keys(enumCls)) {
            if (isNaN(+anEnumKey)) {   // skip numerical keys generated by js.
                const enumValue = enumCls[anEnumKey]
                let enumKeyDesc = descriptor[anEnumKey] as EnumEntryDescriptor
                if (!enumKeyDesc) {
                    enumKeyDesc = {
                        title: enumValue
                    }
                }
                result.entries.push(ExtendedEnumEntry.fromEnumEntryDescriptor(enumKeyDesc, anEnumKey, enumValue))
            }
        }
        return result
    }
}

export interface EnumEntryDescriptor {
    title?: string
    description?: string
}

export class ExtendedEnumEntry {
    title?: string
    description?: string
    key: string
    value: string | number

    constructor(title: string = null, key: string, value: string | number, description?: string) {
        this.title = title ?? key // if title is not provided fallback to key.
        this.description = description
        this.key = key
        this.value = value
    }

    static fromEnumEntryDescriptor(e: EnumEntryDescriptor, key: string, value: string | number) {
        return new ExtendedEnumEntry(e.title, key, value, e.description)
    }
}

Usage

enum testClass {
    foo = "bar",
    anotherFooBar = "barbarbar"
}

const extendedTestClass = ExtendedEnum.from(testClass, { 
    foo: {
        title: "Title for Foo",
        description: "A simple description for entry foo..."
    },
    anotherFooBar: {
        description: "Title here falls back to the key which is: anotherFooBar."
    }
})

Advantages

  • No refactor needed, keep the basic enum structures as is.
  • You get code suggestions for each enum entry (vs code).
  • You get an error if you have a typo in declaring the enum descriptors aka associated types.
  • Get the benefit of associated types and stay on top of OOP paradigms.
  • It is optional to extend an enum entry, if you don't, this class generates the default entry for it.

Disadvantages

This class does not support enums with numerical keys (which shouldn't be annoying, because we usually use enums for human readability and we barely use numbers for enum keys)

When you assign a numerical value to a key; Typescript double-binds the enum key-values. To prevent duplicate entries, this class only considers string keys.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Dave Cousineau
Solution 4 Stefanos Kargas
Solution 5
Solution 6 Hamidreza Vakilian