Enums in typescript

This is a little post about Typescript's utilities & Enum.

In Ruby & python it's straightforward enough to do something like this:

default_prices = {
  "playstation": 500,
  "xbox": 450,
  "switch": 400
}

def get_console_price(unit, type, prices=default_prices):
  return unit * prices['type']

I'm used to being able to pass in optional keyword arguments passing in an object of keys as a third argument

But if you just port over the implied shape of the type:

export type ConsolePrices = {
  [key: string]: number
}

const getPriceBasic = (units: number, type: string, prices: BasicConsolePrices) => {
  return units * prices[type]
}

You miss out on the type checking when you try to use the function:

> console.log(getPriceBasic(4, 'switch', { switch: 50 }))
2000
> console.log(getPriceBasic(4, 'switch', { xbox: 50 }))
NaN
console.log(getPriceBasic(4, 'switch', { ninjas: 50 }))
> NaN

When I've tried to get stricter about typechecking in the past, I've gotten a little confused. There'll be some warnings about how you can't

Here's are an approach I've tried with some success: using enums with some of the built-in utility types

Enums give you a named constant that has some nice properties. 1) It can be typechecked.

enum Console {
  Switch: 'switch',
  Playstation: 'playstation',
  Xbox: 'xbox'
}

// this is fine:
console.log("Console.switch is:", Console.Switch)
> Console.switch is: switch

// this will fail to compile & raise this erro:
// "semantic error TS2339: Property 'Dreamcast' does not exist on type 'typeof Console'."
console.log("Console.switch is:", Console.Dreamcast)

  1. it can be used as an argument parameter
const argument(neat: Console) => {
  console.log("you gave me a ", neat)
}

// this is fine:
argument(Console.Switch)

// yields `2345: Argument of type '"switch"' is not assignable to parameter of type 'Console'.`
argument('switch')

// yields `2339: Property 'Dreamcast' does not exist on type 'typeof Console'.`
argument(Console.Dreamcast)

and 3) it can be used to index other objects. Here I'm using the Record utility type to a type that would have Console keys & their prices. (Essentially the same thing as the original default_prices up above)

export type ConsolePrices = Record<Console, number>

const prices: ConsolePrices = {
  playstation: 500, 
  switch: 400, 
  xbox: 450 
}

It will notice if you give it too many keys or if you're missing one:

// this will yield `2322: Type '{ playstation: number; switch: number; xbox: number...erties, and 'dreamcast' does not exist in type 'ConsolePrices'.`
const prices: ConsolePrices = { playstation: 500, switch: 400, xbox: 450, dreamcast: 2000 }
// this wil yield: `2741: Property 'xbox' is missing in type '{ playstation: number; switch: number; }' but required in type 'ConsolePrices'.`
const prices: ConsolePrices = { playstation: 500, switch: 400 }

But what if you want to omit one or two keys? That's pretty normal. In that case you could use another utilty type, partial, which makes all the fields optional:

const ConsolePrices = Partial<Record<Console, Boolean>>

Now you can write something like this:

const getPrice = (units: number, type: Console, prices: ConsolePrices) => {
  return units * prices[type]
}
const prices: ConsolePrices = { playstation: 500, switch: 400, xbox: 450 }
console.log('price of 5 switches is: ', getPrice(5, Console.Switch, prices))

And it will both work and typecheck & the type errors should be pretty clear. (Much clearer than if you define the types more generically)

You can also do things with keyof, but I don't have time to get into that!