03.2023
webdev
typescript

Why type casting in TypeScript is generally a bad idea?

Explains behind the scenes of TypeScript casting. Why and when it is a bad idea to use them.

In short - what is casting in TypeScript?

Casting is used to tell TypeScript that it should treat a variable as a given type. Even if the type of this variable is known and different.
For example:

const age: number = 25
console.log(age)
console.log(age as string)

There is also another syntax:

const age: number = 25
console.log(age)
console.log(<string>age)

But it is not that common as it collides with TSX.

So, what's wrong with it?

The main reason is that it actually does not change the type of the variable.

TypeScript only treats it as the type you forced it to. When you check the generated JS code you will see exactly that it essentially does nothing to the variable or the final code:

const age: number = 25
console.log(age)
console.log(age as string)

Transpiles to:

"use strict";
const age = 25;
console.log(age);
console.log(age);

As you can clearly see casting is only evaluated during build time and is completely ignored at runtime after being transpiled to JavaScript.
TypeScript will try to warn you that this type of casting is probably a mistake and will prompt you with this warning:

Conversion of type 'number' 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.`

It warns you that these types are totally different from each other and generally shouldn't be casted as one another. But if you look closely it tells you how to suppress the warning. Just slap as unknown first (what could go wrong?):

console.log(age as unknown as string)

Now your spidey sense should be tingling all over the place but for the sake of this article lets roll with it.

const age: number = 25
// TypeScript will rightfully yell:
// "Property 'toUppercase' does not exist on type 'number'."
// which is true, it does not.
console.log(age.toUppercase())
// TypeScript will have no problem with that code
// it treats variable as string and toUppercase() exists on string type.
console.log((age as unknown as string).toUppercase())

But in the end these two lines will fail at runtime:

"use strict";
const age = 25;
// Property 'toUppercase' does not exist on type 'number'.
console.log(age.toUppercase());
// Property 'toUppercase' does not exist on type 'number'.
console.log(age.toUpperCase());

The main problem is that only one of them would prompt an error during type check. Now you can imagine when this TYPE of error (ok I'll stop) is present in much bigger project it can get really messy during runtime.

If it's so bad why is it even a thing?

As always in programming its only bad when you use it wrong. There are legitimate use cases for type casting, and we'll go through two of them.

Narrowing types

First proper use of casting is using them to narrow type of variable. What does that mean?
Take a look at this Vue code snippet:

<input
    :value="modelValue"
    @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)" 
>

We have a code that essentially is v-model="modelValue" but made by hand. (useful when used inside custom components)
Take a closer look at the @input event handler. We are casting $event.target to HTMLInputElement type even when its type is EventTarget.

Why?

Take a look at this type hierarchy below:

|-- EventTarget
|  |-- Node
|  |  |-- Element
|  |  |  |-- HTMLElement
|  |  |  |  |-- HTMLInputElement

You see, HTMLInputElement is a grand grand grandchild (that's a mouthful) of EventTarget so it has all the properties of EventTarget so it won't break anything.

That's cool that it won't break anything but why should I even bother?

Since we know that in this case EventTarget returned by this event handler is actually HTMLInputElement (because it's fired by <input> element). We can make use of improved type safety. Without this type cast we would get and error during type check:

Property 'value' does not exist on type 'EventTarget'.

Because EventTarget does not have value property, but HTMLInputElement does. The code would still work at runtime but by using casting we get rid of the error and improve type safety.

Unit tests

Another use case for casting is in unit tests. This example is much simpler. If you are using Sinon.js library for mocking and stubbing you use this trick:

const classStub = sinon.createStubInstance(SomeClass);
const classThatWeTest = new classThatWeTest(classStub as unknown as SomeClass);

This way we ensure type safety in our tests, and also we can take full advantage of auto-completion in our IDE. Pretty cool, huh?

I should go,

Krystian