121

I have an enum:

export enum PizzaSize {
  SMALL =  0,
  MEDIUM = 1,
  LARGE = 2
}

But here I'd like to use some pair of values: e.g. SMALL I would like to say that it has a key of 0 and a value of 100. I endeavor to use:

export enum PizzaSize {
  SMALL =  { key: 0, value: 100 },
  // ...
}

But TypeScript doesn't accept this one. How can I do this?

Erick Petrucelli
  • 14,386
  • 8
  • 64
  • 84
Vahe Akhsakhalyan
  • 2,140
  • 3
  • 24
  • 38

7 Answers7

191

TypeScript supports numeric or string-based enums only, so you have to emulate object enums with a class (which will allow you to use it as a type in a function declaration):

export class PizzaSize {
  static readonly SMALL  = new PizzaSize('SMALL', 'A small pizza');
  static readonly MEDIUM = new PizzaSize('MEDIUM', 'A medium pizza');
  static readonly LARGE  = new PizzaSize('LARGE', 'A large pizza');

  // private to disallow creating other instances of this type
  private constructor(private readonly key: string, public readonly value: any) {
  }

  toString() {
    return this.key;
  }
}

then you can use the predefined instances to access their value:

const mediumVal = PizzaSize.MEDIUM.value;

or whatever other property/property type you may want to define in a PizzaSize.

and thanks to the toString() overriding, you will also be able to print the enum name/key implicitly from the object:

console.log(PizzaSize.MEDIUM);  // prints 'MEDIUM'
Jérôme Beau
  • 10,608
  • 5
  • 48
  • 52
  • 5
    Way more elegant than the accepted answer but, the constructor should be private constructor(private key: string, PUBLIC value: any) {} to be able to use PizzaSize.MEDIUM.value or define a getter to it – Frohlich Nov 29 '18 at 13:57
  • 1
    True @Flohlich. Just added a getter to forbid change of such an enum constant. – Jérôme Beau Nov 29 '18 at 15:28
  • 1
    this answer is unclear to me, can you show the `enum` declaration not just the `PizzaSize` class? – Alexander Mills Dec 16 '18 at 23:19
  • 1
    @Alexander Mills There is no enum declaration, that’s the point : as there is no support for object enum you have to emulate it through a class declaration. – Jérôme Beau Dec 18 '18 at 07:50
  • 1
    I'd suggest to declare the static items `readonly` as well. Else wise they could be reassigned externally (`PizzaSize.SMALL = PizzaSize.LARGE; // get a big pizza, but pay small :-)`). – qqilihq Feb 19 '19 at 15:55
  • 1
    This is much more elegant. I would use your way too. Upvoted. – Joe Tse Feb 21 '19 at 04:45
  • How can use this in angular html template? – j1s Apr 01 '19 at 12:25
  • @j1s In the relevant component, declare a public property this has the name name as the enum and is equal to it: `PizzaSize = PizzaSize`. Then you'll be allowed to write `PizzaSize.LARGE` in your template. – Jérôme Beau Apr 01 '19 at 12:56
  • What is the purpose of the `toString()` method or the arguments into the constructor? – JeffryHouser Aug 20 '19 at 15:45
  • Nevermind, I figured it out. The internal read only variables are instances of the class. Got it! – JeffryHouser Aug 20 '19 at 17:00
  • 4
    Be careful using this approach if you clone your objects with `Ramda`/`lodash`, e.g `R.clone({pizza: PizzaSize.MEDIUM})` or `_.cloneDeep`. Triple equals and `switch` statements won't work like they do with TypeScript `enum` – Drenai Sep 15 '19 at 15:56
  • 1
    I wish this gave exhaustiveness checks :( That's often the main reason I want to use an enum https://www.typescriptlang.org/play/#code/KYDwDg9gTgLgBAYwDYEMDOa4AUCWAvPFAZX2DgG8AoASDRhRhwTimBQBMIA7JATziIBZAIIAZUXDgBeOF2AB3bPkIk8wABQByIWNGaANHE3C4aALYokSOGGUpNASgDcNOgyYs2nHv0EBRABEASQBVQWlZBSUCYlItf2CwgyMTM2B2HABXMxs7RxdaekZmVg5uPjhRYQAlAHE-SRk5RVwY1Q1NKrq-ZOM4VCgAczJbGPzKGjAoHAA3BjIEbjooTIQYaHUaalLvCoBrYF4ALlMYaa5B-S2d8v45pEzgE5QuXhoHCgBfSm-KADNMlw1jhuHA-uowCdWipSA4TsscBcKK55DgYAgABZwCEAOgOvA+VGo1AQ6DI2hE4k0J1YMEyUC4Rk0W1JaHJCVCgmpnjpDKZLLJRi69W5tPpjM0zOo30+QA – blaineh Sep 24 '19 at 22:24
  • is there no way not to duplicate the literals? ("MEDIUM" is used twice, as a name and then as a parameter) – Tony Oct 04 '22 at 11:05
50

Update: find @Javarome's answer below, which is more elegant. I suggest using his way.

If you need to use Type, try adding some code. usage: getPizzSizeSpec(PizzaSize.small).value

enum PizzaSize {
    small,
    medium,
    large
}
interface PizzaSizeSpec {
    key: number,
    value: number
}
function getPizzaSizeSpec(pizzaSize: PizzaSize): PizzaSizeSpec {
    switch (pizzaSize) {
        case PizzaSize.small:
            return {key:0, value: 25};
        case PizzaSize.medium:
            return {key:0, value: 35};
        case PizzaSize.large:
            return {key:0, value: 50};
    }
}
Joe Tse
  • 593
  • 6
  • 7
  • Check out my recent answer, it also has the benefit of exhausiveness checks – blaineh Jun 20 '19 at 23:05
  • This answer is no way close to object literal enum. https://stackoverflow.com/a/51398471/2103767 is by far the best by @Javarome – bhantol Sep 02 '21 at 15:17
29

As of Typescript 3.4, you can use a combination of keyof typeof and const assertions to create objects that can have the same type safety as enums, and still hold complex values.

By creating a type with the same name as the const, you can have the same exhaustiveness checks that normal enums have.

The only wart is that you need some key in the complex object (I'm using value here) to hold the name of the enum member (if anyone can figure out a helper function that can build these objects in a typesafe way, I'd love to see it! I couldn't get one working).

export const PizzaSize = {
    small: { value: 'small', key: 0, size: 25 },
    medium: { value: 'medium', key: 1, size: 35 },
    large: { value: 'large', key: 2, size: 50 },
} as const

export type PizzaSize = keyof typeof PizzaSize

// if you remove any of these cases, the function won't compile
// because it can't guarantee that you've returned a string
export function order(p: PizzaSize): string {
    switch (p) {
        case PizzaSize.small.value: return 'just for show'
        case PizzaSize.medium.value: return 'just for show'
        case PizzaSize.large.value: return 'just for show'
    }
}

// you can also just hardcode the strings,
// they'll be type checked
export function order(p: PizzaSize): string {
    switch (p) {
        case 'small': return 'just for show'
        case 'medium': return 'just for show'
        case 'large': return 'just for show'
    }
}

In other files this can be used simply, just import PizzaSize.

import { PizzaSize } from './pizza'

console.log(PizzaSize.small.key)

type Order = { size: PizzaSize, person: string }

Also notice how even objects that are usually mutable can't be mutated with the as const syntax.

const Thing = {
    ONE: { one: [1, 2, 3] }
} as const

// this won't compile!! Yay!!
Thing.ONE.one.splice(1, 0, 0)
blaineh
  • 2,263
  • 3
  • 28
  • 46
  • 1
    +1 for the `const ... as const`, that's a new one for me! This approach is ok, but it's a bit more difficult to reason about than Javarome's – Drenai Aug 05 '19 at 21:16
  • It does seem a little tricky. And it doesn't allow you to add functions to the "enum" values like the class does. But this give exhaustiveness checks where the class doesn't, so it's just a choice of tradeoffs I'm afraid. – blaineh Sep 24 '19 at 22:27
  • 2
    FYI my tsc `3.9.7` is complaining about `order(PizzaSize.large)` with `Argument of type '{ readonly value: "large"; readonly key: 2; readonly size: 50; }' is not assignable to parameter of type '"small" | "medium" | "large"'`. So it doesn't behave like enums in that way. – Basti Sep 07 '20 at 16:07
  • Yeah this solution isn't exactly like enums, it's just parallel, using the keys of a `const` object instead of an actual enum value. Pass `order(PizzaSize.large.value)` or `order('large')` and everything will compile. Doing that is just as typesafe as an enum. – blaineh Sep 07 '20 at 19:42
  • In terms of building the values of the enumerated objects in a typesafe way, the best I could think to do was to cast each declaration as I defined it. It's a little more verbose than I'd like, but it seems to get the job done. – Dennis Sep 01 '21 at 19:52
  • You can reference this pattern as enumeration classes – EliuX Jul 14 '22 at 14:47
21

I think to get to what you want, something like this will work

interface PizzaInfo {
  name: string;
  cost_multiplier: number;
}

enum PizzaSize {
  SMALL,
  MEDIUM,
  LARGE,
}

const pizzas: Record<PizzaSize, PizzaInfo> = {
  [PizzaSize.SMALL]: { name: "Small", cost_multiplier: 0.7 },
  [PizzaSize.MEDIUM]: { name: "Medium", cost_multiplier: 1.0 },
  [PizzaSize.LARGE]: { name: "Large", cost_multiplier: 1.5 },
};

const order = PizzaSize.SMALL;
console.log(pizzas[order].name);  // "Small"
Ben
  • 560
  • 4
  • 6
4

Object.freeze makes it read only and prevents more properties being added:

const pizzaSize = Object.freeze({
  small: { key: 0, value: 25 },
  medium: { key: 1, value: 35 },
  large: { key: 2, value: 50 }
})
danday74
  • 52,471
  • 49
  • 232
  • 283
3

You can use a typed const to achieve this:

export const PizzaSize: {
    [key: string]: { key: string, value: string };
} = {
    SMALL: { key: 'key', value: 'value' }
};

Optionally you can extract the type information to separate interface declarations:


interface PizzaSizeEnumInstance {
    key: string;
    value: string;
}

interface PizzaSizeEnum {
    [key: string]: PizzaSizeEnumInstance;
}

export const PizzaSize: PizzaSizeEnum = {
    SMALL: { key: 'key', value: 'value' }
};
Mark Lagendijk
  • 6,247
  • 2
  • 36
  • 24
0

Old thread, but I came up with something similar to @Jerome. It's my attempt at making a sealed class

module SealedPizzaEnum {
    abstract class PizzaBase {
      constructor(public topping: string) {}
    }
    export type Spec = PizzaBase;

    export class Cheese extends PizzaBase {}
    export class ThreeMeat extends PizzaBase {
        addMoreMeat() {
            console.log("Meeaaattt");
        }
    }
}

function makeADaPizza(pizza: SealedPizzaEnum.Spec) {
    if(pizza instanceof SealedPizzaEnum.Cheese) {
        console.log(pizza.topping);
    } else if (pizza instanceof SealedPizzaEnum.ThreeMeat) {
        pizza.addMoreMeat();
    }
}

makeADaPizza(new SealedPizzaEnum.Cheese("pineapple"));
makeADaPizza(new SealedPizzaEnum.ThreeMeat("Onion"));
Cole
  • 1
  • 1