154

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.

10 Answers 10

177

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);
Sign up to request clarification or add additional context in comments.

8 Comments

Is there a way to add a method to the enum member?
@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 - 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?
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.
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. 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
|
39

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
    }
}

7 Comments

import Mode = require('Mode'); ... /*more code here...*/ <Mode.landscape> landScape = Mode.landscape; and I got error: error TS2503: Cannot find namespace 'Mode'
@pawciobiel Well, I guess Mode is the type here you want to cast to and not landscape. Change your cast from <Mode.landscape> to just <Mode>
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 It works unchanged on typescriptlang.org/play (adding the line Mode.doSomething(Mode.landscape); at the end for testing)
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.
|
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;
   }
}

4 Comments

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.
@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.
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 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.
14

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.

Comments

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

Comments

4

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.

Comments

0

A different approach, maybe not the better one if you need to create a lot of enums:

export class EnumClass {
  static [key: number]: EnumClass 
  static VALUE1 = new EnumClass(0)
  static VALUE2 = new EnumClass(1)

  private constructor(private index: number) {} // could be anything

  static values(): EnumClass[] {
    return [
      EnumClass.VALUE1,
      EnumClass.VALUE2,
    ]
  }

  label() {
    switch (this.index) {
      case 0:
        return "Value 1"
      case 1:
        return "Value 2"
    }
  }
}

So you can call the methods like:

EnumClass.values() //static method

or:

EnumClass.VALUE1.label() //class method

Comments

0

Based on @fenton solution, but possible to do Object.keys(Color)

export enum Color {
  Green = "GREEN",
  Red = "RED",
}

export namespace Color {
  export declare const values: readonly Color[];
  export declare function stringify(mode: Color): string;
}

Object.setPrototypeOf(Color, {
  values: Object.values(Color),
  stringify: (color: Color): string => {
    switch (color) {
      case Color.Green:
        return "Зеленый";
      case Color.Red:
        return "Красный";
    }
  },
});

Comments

0

It is possible with const enum. Here is the example what solution I come up with:

type Color = (typeof Color)["values"][number];
const Color = declareEnum({
  Green: "GREEN",
  Red: "RED",

  stringify(value: Color): string {
    switch (value) {
      case Color.Green:
        return "Зеленый";
      case Color.Red:
        return "Красный";
      default:
        value satisfies never;
        return value;
    }
  },
});

// Usage
console.log(Color.Green); // GREEN
console.log(Color.Red); // RED
console.log(Color.enum); // {Green: 'GREEN', Red: 'RED'}
console.log(Color.keys); // ['Green', 'Red']
console.log(Color.values); // ['GREEN', 'RED']
console.log(Object.keys(Color)); // ['Green', 'Red']
console.log(Object.values(Color)); // ['GREEN', 'RED']
console.log({ ...Color }); // {Green: 'GREEN', Red: 'RED'}

let a: Color = Color.Green;
a = Color.Red;
const b: typeof Color.Red = Color.Red; // if you want to provide specific keys, use `typeof`

// calling a function
console.log(Color.stringify(Color.Green)); // Зеленый
console.log(Color.stringify(Color.Red)); // Красный
console.log(Color.stringify("NOTAMEMBER" as Color)); // NOTAMEMBER

// Just an example of how to use it in third parties, like zod

z.nativeEnum(Color.enum);
z.enum(Color.values);

The code of declaring enum looks like this:

type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;

type LastInUnion<U> = UnionToIntersection<U extends any ? (x: U) => void : never> extends (x: infer L) => void
  ? L
  : never;

type UnionToTuple<U, T extends readonly any[] = []> = [U] extends [never]
  ? T
  : UnionToTuple<Exclude<U, LastInUnion<U>>, readonly [LastInUnion<U>, ...T]>;

type EnumMeta<Enum> = {
  readonly enum: Enum;
  readonly keys: UnionToTuple<keyof Enum>;
  readonly values: UnionToTuple<Enum[keyof Enum]>;
};

export function declareEnum<
  Value extends string,
  Input extends {
    readonly [Key in Capitalize<string>]: Key extends Capitalize<string> ? Value : unknown;
  },
  Enum extends {
    readonly [Key in keyof Input as Extract<Key, Capitalize<string>>]: Input[Key];
  }
>(input: Input): Input & EnumMeta<Enum> {
  const output = {} as any;
  for (const key in input) {
    if (Object.prototype.hasOwnProperty.call(input, key)) {
      Object.defineProperty(output, key, {
        value: input[key],
        enumerable: key.charAt(0) === key.charAt(0).toUpperCase(),
      });
    }
  }

  Object.defineProperty(output, "enum", { value: output });
  Object.defineProperty(output, "keys", { value: Object.keys(output) });
  Object.defineProperty(output, "values", { value: Object.values(output) });

  return output;
}

The full code is in gist: https://gist.github.com/Chiorufarewerin/df5b61672c6bd3190319b5fd9bc6d245

Comments

0

This is what I am using in the end, it may not be very sexy, but it is efficient :

export enum QuestionType {
  Boolean = 'BOOLEAN',
  Binary = 'BINARY',
  Quizz = 'QUIZZ',
}

export const QuestionTypeFrom = (enumObj: QuestionType) => ({
  toString: () => {
    switch (enumObj) {
      case QuestionType.Boolean:
        return 'True / False'
      case QuestionType.Binary:
        return 'Yes / No'
      case QuestionType.Quizz:
        return 'Multiple Choice'
      default:
        throw new Error(`Unhandled QuestionType: ${enumObj}`)
    }
  },
})

QuestionTypeFrom(QuestionType.Boolean).toString()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.