We've talked about refactoring lying TypeScript type assertions into real assertions before in the context of the DOM API. But there is a more common use-case with union of string literals.
An union of string literals is often use to describe configuration options. For example, Vite log levels are 'info' | 'warn' | 'error'
. But what is the best way in TypeScript to convert a string into a type?
Let's say we are given a string
and want to convert it into a LogLevel
. We want to use the LogLevel
in a switch statement.
To convert a string into a LogLevel, the simplest way is to use type assertion. Type assertion is the syntax of the form value as Type
. It is hard to Google for if you didn't know the term. Lots of people mistaken type assertion as "type casting".
The type assertion value as Type
means is, "I, as the human, assure you, TypeScript, that value
is a Type
". TypeScript will complain if it is impossible for value to be a Type, for example such as true as string
would never be allowed. But in this case TypeScript sees value is a string and believes the human made type assertion.
Continuing our example, here is what not to do.
We pass in an invalid log level "debug" and TypeScript happily believes it is a LogLevel. It is not until we use the invalid LogLevel does our code fail.
Also notice this cause TypeScript to break its promise of handling switch cases exhaustively. The previously impossible default case gets triggered. Our bad promise to TypeScript cause TypeScript in turn to break its promise back to us. But let's be clear here, we are at fault. We are the ones abusing TypeScript, not the other way around.
Type assertion is like using a sledgehammer to drive a nail. What is the more appropriate hammer?
Assertion function is more appropriate in this situation. The syntax is much more involved. (value: unknown) => asserts value is Type
, means "I, as the human, assure you, TypeScript, that the body of this function will return void
if the value
is a Type
, otherwise I will throw an error". That is a much more precise promise and requires the human to actually validate value
.
What this looks like in code.
Now we detect the bad LogLevel early and the default case in consumeLogLevel
is back to being impossible.
While it is important to understand how TypeScript works, lots of what is described here is better implemented with Zod enum.
Do you enjoy your switch cases being exhaustive? You're in luck, Battlefy is hiring.