DEV Community

loading...

Short and Sweet: Typescript const assertions

Emanuel Lindström
・3 min read

I will try to communicate, as short and sweet as possible the pros of using const assertions instead of Enums in Typescript. Let's begin.

First we define the enum-like values, and make a type from them.

const fruits = ['APPLE', 'BANANA', 'ORANGE'] as const;  // This is the const assertion
type Fruits = typeof fruits[number]; // type Fruits = "APPLE" | "BANANA" | "ORANGE"
Enter fullscreen mode Exit fullscreen mode

This is the basic setup. The list and the type will act like a very useful enum-like thing for us to use.

You can use the enum-like values like so:

const myFruit = fruits[0]  // const myFruit: "APPLE"
// Notice how the type is not cast to the general "string" but the much narrower string "APPLE"
Enter fullscreen mode Exit fullscreen mode

You can verify a string as being of the defined type with a type predicate. The principle is that you have a function that returns a boolean that says if the predicate is true or false. Typescript will use the predicate to infer the type for you and your IDE.

const items = ['APPLE', 'iPhone']

const isFruit = (value: string): value is Fruits => {
  return fruits.includes(value)
}  // The "value is Fruits"-part is a 'type predicate' which lets Typescript cast the type (used in the if-else statement below)

isFruit(items[0])  // returns true
isFruit(items[1])  // returns false

items.forEach(item => {
  if(isFruit(item)){
    console.log("My fruit: ", item)  // item: "APPLE" | "BANANA" | "ORANGE"
  } else {
    console.log("My thing: ", item)  // item: string
  }
})
Enter fullscreen mode Exit fullscreen mode

The type assertion function can be anything that verifies that the input is of a certain type. You can check that it has certain properties or do whatever checks you want. I'm checking if it's included in a list, but you could also check if fish go blub... or any other sounds.

You can also do basic, neat stuff like this now:

const fruitBowl: Fruits[] = []
fruitBowl.push('APPLE')  //works
fruitBowl.push('iPhone') // Argument of type '"iPhone"' is not assignable to parameter of type '"APPLE" | "BANANA" | "ORANGE"'
Enter fullscreen mode Exit fullscreen mode

That's it.
What we're doing is allowing Typescript to guarantee that our values are of a certain type and pass that information on to the IDE. The IDE uses that type information to stop us from adding non-fruit items to our fruitBowl and also give us helpful type information inside the if-statement after the type predicate returns true.

So, when do we use enums?

An enum is great if you just want to write hard coded values and use them throughout your code. For instance, you might want to use certain information for your company.

enum CompanyInfo {
   CEO = "John Doe"
   SLOGAN = "My Company Slogan"
   ADDRESS = "Main Street"
}
Enter fullscreen mode Exit fullscreen mode

You can use those values in many places in your code and then only update the enum if any of the information changes in the future. That's basically the best use case for enums in Typescript, as I see it. For everything else, const assertions is probably a better choice.

Bonus:

If you don't like the fruits[0] syntax for accessing the values, you could also define your const asserted list as actual enum values. Then the setup would look something like this:

enum FruitsEnum {
  APPLE = "APPLE",
  BANANA = "BANANA",
  ORANGE = "ORANGE"
}
const fruits = [FruitsEnum.APPLE, FruitsEnum.BANANA, FruitsEnum.ORANGE]

type Fruits = typeof fruits[number]
Enter fullscreen mode Exit fullscreen mode

Now you have the values accessible in a neat enum, a list to check those values against and a type based on the list values.

Do you have another way to solve this? Tell me your solution in the comments. :)

Discussion (0)