137

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.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Mr. Baudin
  • 2,104
  • 2
  • 16
  • 24

6 Answers6

165

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);
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • 11
    Is there a way to add a method to the enum member? – Jay Wick Sep 16 '15 at 00:05
  • Any advantages to using module over class? – Daniel Feb 26 '18 at 15:11
  • 2
    @Daniel - I have updated the answer as `module` is now `namespace`. I have also highlighted the key benefit of a namespace in this particular case, which is the methods appear under the enum, i.e. `Mode`. – Fenton Feb 26 '18 at 15:23
  • 2
    @Fenton - I'd like to `import { Mode } from "./Mode.ts"` and use it in the same way as in your usage example - is this possible with namespaces merged? – Vedran Apr 10 '18 at 13:50
  • if you wish to use the said enum somewhere else, you just need to export everything. The enum itself and the namespace/functions you added. Might not be obvious maybe... – Cubelaster Mar 04 '19 at 09:50
  • 8
    worth noting that the `namespace` approach will potentially cause problems in that functions that consume the "enum" (for example to iterate enum members to populate a select list) will produce unexpected results like "myAddedMethod() {...}". You can filter out extraneous non-enum members at the iteration point, but just pointing out that this approach isn't without drawbacks. – virtualeyes Apr 10 '19 at 03:59
  • that namespace merge is mentioned in the documentation but it simply doesn't work, oddly – R-D Jul 23 '21 at 16:56
  • Is there any ES15 compliant way to add functions to enums? We have rather strict linting rules, i.e. we are not allowed to declare modules/namespaces due to [this linting rule](https://github.com/typescript-eslint/typescript-eslint/blob/v4.30.0/packages/eslint-plugin/docs/rules/no-namespace.md). While importing that function directly is obviously a working solution, I don't find it to be the best approach when the function should obviously be paired with the enum – Roman Vottner Oct 13 '21 at 12:53
40

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
    }
}
Benjamin
  • 1,165
  • 7
  • 19
  • 1
    `import Mode = require('Mode'); ... /*more code here...*/ landScape = Mode.landscape;` and I got error: `error TS2503: Cannot find namespace 'Mode'` – pawciobiel Sep 09 '15 at 13:43
  • 1
    @pawciobiel Well, I guess `Mode` is the type here you want to cast to and not `landscape`. Change your cast from `` to just `` – Benjamin Sep 10 '15 at 08:25
  • just a note that this does not work in TS 1.8.10. i had to add an `export` in front of the module definition. – icfantv Jun 02 '16 at 23:04
  • @icfantv It works unchanged on http://www.typescriptlang.org/play/ (adding the line `Mode.doSomething(Mode.landscape);` at the end for testing) – Benjamin Jun 03 '16 at 08:44
  • 1
    I think it was because my enum also had `export` and the issue is that in order for TS to merge the two objects, they must either both be exported or both not be. Sorry for the confusion. – icfantv Jun 03 '16 at 13:02
  • Is it possible to implement ```doSomething``` in such a way that you can call it like ```Mode.landscape.doSomething()```? (Like an extension method in C#) – Koja Jun 27 '18 at 06:59
  • @Koja TypeScript enums are based on primitive values, not objects. So does not make sense with this approach. You could do this with the [enum pattern approach](https://stackoverflow.com/a/47443987/1859442) instead. – Benjamin Jul 05 '18 at 11:13
22

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;
   }
}
Dave Cousineau
  • 12,154
  • 8
  • 64
  • 80
  • 1
    Your own example can be seen as a counterargument to your point. Height/width often times vary based on other things, not only "landscape vs portrait" - they don't belong to this enum. This enum really expresses only a vague preference, its exact meaning depends on the context. – Vsevolod Golovanov Mar 31 '20 at 08:27
  • 4
    @VsevolodGolovanov sure, it's just an example demonstrating how to set it up. I'm not suggesting to use a `Mode` enum with these values. use whatever values are relevant to your project. – Dave Cousineau Mar 31 '20 at 18:33
  • What @VsevolodGolovanov is saying, I think, is that a value object (as you suggested), is not an enum and that regardless what values you use, they won't really belong to the enum. – Christian Jun 04 '20 at 14:24
  • 2
    @Christian he was saying that "width", "height", and "aspect ratio" are not really properties of "landscape" and "portrait"; ie: the concept of landscape is independent of size. this is true, but it's just an example. – Dave Cousineau Jun 04 '20 at 16:47
13

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.

Stefanos Kargas
  • 10,547
  • 22
  • 76
  • 101
4

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();
david valentino
  • 920
  • 11
  • 18
2

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.