DEV Community

Cover image for Typescript cast is a type breaker
Yann L
Yann L

Posted on • Updated on

Typescript cast is a type breaker

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
Enter fullscreen mode Exit fullscreen mode

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.

Example of type error highlighted by TS

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
Enter fullscreen mode Exit fullscreen mode

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:

A type error not highlighted by Typescript due to the cast

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.

Cast to precise inherited object type

You're precising the type from union types

The same can also be done when you're defining the type from a union type.

Cast to precise from 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');
Enter fullscreen mode Exit fullscreen mode

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.

Example of guards before cast

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.

Example of type predicates function

 

const possibleFish = { swim: () => console.log('swim') };
if (isFish(possibleFish)) {
   // here the variable is infered as a Fish
Enter fullscreen mode Exit fullscreen mode

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:

Example of cast of different objects without unknown type

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)
Enter fullscreen mode Exit fullscreen mode

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

Example of a cast with unknown type

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 🤘

Latest comments (0)