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