Avoid using Type Assertions in TypeScript
In this issue, I will explain why you should avoid using Type Assertions in Typescript and techniques you can use to avoid them.
We have all been there, and we have come across the following error:
And we have been inclined to solve the above issue by asserting the type.
example(y as string)
So, why am I discouraging you to avoid doing this? Let me answer that question in this issue of All Things Typescript.
But first, what are Type Assertions?
Types assertion is a way of telling Typescript what the type of a variable is. This can be done either of two ways: using the as
syntax or the angle bracket <Type>
syntax, as shown below:
type Person = {
firstname: string;
lastname: string;
}
// using as syntax
const x : unknown = {};
// asserting it as Person using as syntax
const firstname = (x as Person).firstname;
// asserting it as Person using the angle brackets
const firstname = (<Person>x).firstname;
Type assertion allows us as developers to provide more information to Typescript about a variable type. This is a good way to go when we have more information about a variable Type than Typescript is able to infer based on the limited information available to it. For instance, dom elements:
const button = document.getElementById("btn");
The type of button above is HTMLElement, which is generic. But we know that the type should be HTMLButtonElement
and we can use assertions to provide more information to Typescript, as shown below:
const button = document.getElementById("btn") as HTMLButtonElement | null;
As you can imagine, this is a very powerful feature and can come in handy as shown above. But with power comes great responsibility and I believe that assertion should be used sparingly and not the default option.
When we use type assertion we are basically telling the Typescript compiler that we know what the type is and it should trust us, i.e. we know what we are doing. The problem with this is that we prevent Typescript from helping us where it should and take on that responsibility ourselves.
In the above example, Typescript does not type-check whether the variable x
has the property firstname
we are accessing because we are asserting the type, which will definitely introduce a bug into our system.
Typescript does provide some safeguards and will warn you when you try to perform an unsafe type assertion, as shown below:
function example(x: string) {
// do something
}
let y: number | undefined;
example(y as string)
In the case above, Typescript will warn you against performing a type assertion that’s not structurally sound.
Conversion of type 'number | undefined' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type 'number' is not comparable to type 'string'.(2352)
Of course, if you are still determined, you can still override Typescript by first asserting it as unknown and then to string, as shown below:
example(y as unknown as string)
One other thing to keep in mind is that if you are asserting to any, you will completely disable type-checking for that variable usage. This is because the any
type is assignable to any variable and can be assigned to the any
variable. This means any is dangerous as it can be contagious, due to its ability to be assigned to any variable and vice versa.
What about Non-null Assertions?
Another common type of assertion is a non-null assertion. In this assertion, we use the !
operator after variable to tell the Typescript compiler that a variable isn't null.
function square(x: number) {
return x * x;
}
const x : number | undefined;
const answer = square(x!);
This assertion should be used sparingly, especially if the null suggestion is coming from external API typing like environment variables, which are always typed as string | undefined
. I have come across not-so-obvious bugs that were thrown in a completely different section of the code with a different error message because I allowed an undefined variable to be passed on. This happened because instead of handling the possibility of the environment variable being undefined, I decided that non-null assertion was the way to go.
So, what are the Alternatives?
Narrowing of Types
Type narrowing is the process of moving a less precise type to a more precise type. For instance, taking a variable of type any
and moving it to string. There are various ways of achieving this, which I have covered previously here, but I will take a look at a few notable ones.
Type Guards: You can use Type Guards to narrow the types of a union
, unknown
, any
, etc. to a specific type:
function doSomething(x: string | number) {
if(typeof x === "string") {
// do somethign with the string
} else {
// do something with the number
}
}
Truthiness Narrowing: You can check if a variable is truthy i.e. not undefined or null before using it:
function doSomething(x?: string) {
if(x) {
// type of x is now string
}
}
Building Custom Type Guards: Finally, you can create type guards that do an exhaustive type checking on an object before asserting its type:
function isRectangle(shape: unknown): shape is Rectangle {
if ("width" in shape && "height" in shape) {
// this is a rectangle
return true;
}
// it's not a rectangle
return false;
}
Providing Default/Fallback Values
This mostly works with null and undefined values, but instead of asserting to a string to remove the possibility of it being undefined, you can provide a default value that automatically becomes a string. You can achieve this by using either null coalescing operator (??
) or the or ( ||
) operator.
// using the nullish coalescing operator
const API_URL = process.ENV.API_URL ?? "DEFAULT URL";
// using the OR (||) logical operator
const API_URL = process.ENV.API_URL || "DEFAULT URL";
We can also use Javascript Logical Assignment Operator to provide a default value:
let x : string | number;
// provide a default value if null or undefined
x ??= "Hello World"
// provide a default value if falsy
x ||= "Hello World"
Conclusion
In today’s issue, we learned that by using type assertions, we are limiting the ability of the Typescript compiler to do Type checking for us. This can lead to us having bugs in our system which Typescript was meant to prevent. To avoid using assertions, we covered a few techniques we can use such as Type narrowing and providing fallback values in the case of possibly undefined/null values.
That’s it for this week, and here are a few more issues you might interested in: