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
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.