Why You Should Avoid Type Assertions in TypeScript

Why You Should Avoid Type Assertions in TypeScript
Photo by Estée Janssens on Unsplash

One of TypeScript’s primary design goals is to “Statically identify constructs that are likely to be errors.”

While TypeScript introduces type safety which eliminates a plethora of bugs at compile-time, it also gives us the power to override the compiler’s type checker by using a language feature called type assertion. While type assertions can be very powerful when used in the right use case, we should remember that “With great power, comes great responsibility”!

Type assertion in TypeScript is the as syntax and angle-bracket syntax made available by TypeScript to ‘assert’ any TypeScript identifier to a type of the implementer’s choosing. However, this can easily be misused and lead to anti-patterns, brittle code requiring shotgun surgery.

It weakens Type Safety

Example of type assertion using either of the two syntaxes :

export interface Person {
    name: string;
    age: number;
    occupation: string;
}
//here we are casting the object literal to Person using the `as` syntax for type assertion
const john = {
    name: 'John',
    age: 25,
} as Person
//here we are casting the object literal to Person using the `ang-bracket` syntax of type assertion
const william = <Person>{
    name: 'William',
    age: 25,
};

It is essentially telling typescript to “stop type checking and trust me, I know what I’m doing”.

This can lead to the weakening of type safety which is the core purpose of using TypeScript!

Now you might have noticed that occupation is actually missing in the object literal we have created above but this code will compile without errors!

Because of this, type assertions are considered an anti-pattern or code smell unless the user is absolutely sure what they are doing. For example, in some advanced/fancy use case of type hacking, tooling or when the typings of a third-party library is not accurate. Read more about type assertions here.

Valid Use Cases

  • Here is an example of a valid use case of the type assertion
const deserialize = <T>(data: string): T => JSON.parse(data) as T;

const jake = deserialize<Person>('{"name":"Jake", "age":24, "occupation": "artist"}');
 

This is a utility method that lets you deserialize a JSON string to a known Type. This can be useful when deserializing WebSocket payloads for instance.

Hovering over jake shows us that the compiler’s type checker considers jake of type Person

  • Here is another valid use case :
element.addEventListener('click', event => {

    let mouseEvent = event as MouseEvent;
    
});

Here we are asserting that event is of type MouseEvent which seems to be a valid use case.

Use Type Annotations Instead

So if we are not going to allow type assertions on object literals what do we do? We should use Type Annotations over Type Assertions. This enforces type safety and will cause compile-time errors if the declared type is missing any properties or wrongly typed.

// bad :( 
const john = {
    name: 'John',
    age: 25,
} as Person

// bad :(
const william = <Person>{
    name: 'William',
    age: 25,
};

Do this instead:

// Good :)
const william: Person = {
    name: 'William',
    age: 25,
    occupation: 'artist'
};

This way if the object literal is missing any properties it will throw a runtime error because the typing is wrong, and this is desired!

Having said that, sometimes we can’t avoid using type assertions. In these cases, it is considered better to use the as syntax over the<> angle-brackets syntax because the as syntax is more transparent.

Add Linting rules to disable Type Assertions

It is difficult to cut out anti-patterns once they have become a habit. This is where linting can be very useful by giving us visual feedback in our code editor and enabling us to detect and avoid code smells. We can easily add TSLint rules to warn us of unnecessary type assertions. Add the following rules to your project’s tslint.json file.

//tslint.json
...
"rules": {
    "no-unnecessary-type-assertion": true,
    "no-object-literal-type-assertion": true,
    "no-angle-bracket-type-assertion": true,
}
...

Alternatively, if you use ESLint you can find similar rules for ESLint in the @typescript-eslint package

Now that we have the linting rules configured, we can detect and avoid unnecessary type assertions in our codebase and keep our code more maintainable!

If we get rid of required properties and type assertions it gives us a linting error but the code will still compile without errors! (type assertion weakens type safety 😞)

But if we use type annotations at declaration time, the TypeScript compiler will throw errors in case we use wrong typing (stronger type safety 😊)

Also hovering over the compile-time error identifier in VSCode tells us which properties are missing/wrong

This helps us to eliminate a whole class of errors by breaking our build at compile time.

Summary

TypeScript adds type annotations and documentation to JavaScript and helps us write scalable and maintainable applications. If you want to learn more about TypeScript check this book out.

In this article, we learnt how type assertions weaken type safety when used incorrectly. We also saw some valid use cases of type assertions and learnt how to enable TSLint rules to detect and avoid type assertion code smells in our codebase.

Thank you for reading, hope this helps you as it helped me. Let me know in the comments below :)

You can follow me on GitHub and Twitter

Happy Engineering!