Use object literal as TypeScript enum values

AngularTypescriptEnums

Angular Problem Overview


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?

Angular Solutions


Solution 1 - Angular

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'

Solution 2 - Angular

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

Solution 3 - Angular

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)

Solution 4 - Angular

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"

Solution 5 - Angular

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

Solution 6 - Angular

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionVahe AkhsakhalyanView Question on Stackoverflow
Solution 1 - AngularJavaromeView Answer on Stackoverflow
Solution 2 - AngularJoe TseView Answer on Stackoverflow
Solution 3 - AngularblainehView Answer on Stackoverflow
Solution 4 - AngularBenView Answer on Stackoverflow
Solution 5 - Angulardanday74View Answer on Stackoverflow
Solution 6 - AngularMark LagendijkView Answer on Stackoverflow