Typescript cast is a type breaker
Yann L
Posted on June 17, 2021
Cover image by Pankaj Patel on Unsplash
The Typescript types and "cast"
Everyone is aware that the any
type is a bad thing because it would basically be like coming back to Javascript where there's no strong type check and anything is allowed.
But even if we respect this rule, we can fall into another pitfal where we break Typescript's type-check by casting types. Following chapter will describe you why and how to avoid it.
Typing the code
Type creation can be done in several ways in typescript. It can be anonymous, interface/class declarations, type aliases, duck-typings, etc. (See everything in the doc)
But when it comes about typing a variable, there's one syntax only which is with colon :.
const mystring: string = 'hello'; // this example could be duck-typed, but yeah, focus on the colon
By typing a variable, TS will know what type it is, and check that the content of the variable is valid with the given type.
The following example shows exactly this behavior, the compiler will tell you that your definition is not valid according to the type you've defined.
Casting the code
The cast syntax is done either with the as
keyword, or with the angle-bracket notation <...>
. The former being probably better (opinionated) as it can't be confused with the angle bracket notation for generic types.
const duck = animal as Duck; // 'as' keyword notation
const duck = <Duck>animal; // angle bracket notation
If instead of typing it, you use a cast, TS will know the type of your object, but won't make any type check, because casting the variable is about saying to TS "Trust me, I know what I'm doing!".
The same example casted would then look like following and wouldn't raise any error:
Needless to say that this type check "break" is where it becomes dangerous and that's why we should not confuse types and casts.
In fact it was so confusing that even TS developers have added a new feature (the unknown
type described later in this page) to prevent some unwanted casts and force developers to explicitly cast completely different types.
When is a cast ok?
Like in every other strongly typed languages, a cast in the code is mostly only about polymorphism.
Here are some example:
You're precising the type with inheritance
Like in other languages, that's basically the reason for a cast. When you have an object of a certain type group and want to precise the exact type because you now know more about it.
You're precising the type from union types
The same can also be done when you're defining the type from a union type.
You're infering a type from an external input
It can be that a type comes from an external input and you have no idea what type it is. In that case, it's a mix between both previous reason.
As an example, it could be a DOM API like the following where you specify the precise type in a generic syntax
// here TS won't know what element you're accessing. So it will by default use HTMLElement | null
// But we can cast it to what we know it is with the angle-bracket syntax
const aFieldInTheHTML = document.getElementById<HTMLInputElement>('username-field');
Best practices when doing a cast
Always Guard
One thing to not forget is that the cast is only existing in typescript. Once compiled, it's javascript and there's no mention of the type anymore. For that reason, casting is not enough and you have to first check explicitly (= Guard) the value to ensure it is what you think it is.
Type predicates and guard functions
To help you writing guards and cast in one shot, Typescript developers have created type predicates like the is
keyword below (See official documentation).
By using them, simply calling this function will cast the variable automatically and correctly from where the guard is called by refering to the returned boolean.
const possibleFish = { swim: () => console.log('swim') };
if (isFish(possibleFish)) {
// here the variable is infered as a Fish
The unknown
type to explicitly force cast
In recent release of TS (v. 3+), they've added a new type: unknown. (See official documentation).
This type is used to define a type that we don't know what it is, but where we don't want to use any
to not completely "disable" type check.
New TS version will also force to use this type to prevent you to cast completely different object, as doing so would look like a buggy implementation. And if you really have a good reason to do so, then you're adding an additional cast to unknown to make everyone aware it's intended.
Let's think about a use case where you want to cast an object to another, but you know they've nothing in common. It's not from a union type, it's not from inheritance, it's just completely different, like casting a pizza into a tree: 🍕 ▶ 🌳.
It's something that should probably never happen, and that's why TS doesn't allow it as easily as before. If you try it, here's how it will look like:
with the following error
Conversion of type '{ price: number; size: number; ingredients: string[]; }' to type 'Tree' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Type '{ price: number; size: number; ingredients: string[]; }' is missing the following properties from type 'Tree': color, height, age(2352)
Typescript detected that this cast is probably a mistake, and tells it to you. If now for a mysterious reason, you think it's totally ok to cast a 🍕 ▶ 🌳, you can force that cast by casting into unknown first. This will tell the compiler "Trust me, I know it looks wrong, but YOLO!!".
Conclusion
Typescript is about typings, but you always have a way to break the typing for good or bad reasons. So here are some key advices.
- Always type (not cast) all your code but forget the
any
as much as possible. - If you have to cast, it's a polymorphism-like reason, or it's probably a code smell.
- If you want to cast for another reason and you're sure it's justified, then use the
unknown
type, and maybe add a comment about the reason for future readers 🙏.
Last but not least, don't forget one important rule:
Typescript is only about types in your IDE/compiler. Once it's running in the browser/engine there's external factors in the game, and nothing will prevent your code to be of the wrong type! => be defensive.
Tell me in comments 💬 if you see other valid use cases for casts than the one listed above.
Thanks for reading 🤘
Posted on June 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.