24 July 2019

TypeScript Gotchas: Type Assertions

TL;DR

In summary, unless you want lots of weird non-checking of your types avoid mixing type assertions and literals. The critical thing to remember is that when you use a type assertion you're telling TypeScript that you know what the type is and it doesn't need to check. If there is genuinely no way TypeScript can check the value, i.e. it's a runtime value; maybe it came back from a web service, then it's valid to use the assertion (possibly after you do your own validation). But if you're writing a literal value in TypeScript then the compiler knows everything about the value and the context so you shouldn't assert anything and should leave the compiler to do its job in peace.

The details...

I think TypeScript is the best thing to happen to JavaScript since Douglas Crockford's JavaScript: The Good Parts and the linters which checked your code against its recommendations. However in many ways TypeScript is much more a security blanket than a safety net as it's easy to break its type checking without realising. I'm going to dive in to a few of the most common gotchas (in my limited experience using it) in a couple of blog posts. This first post is reserved for, what I consider the worst offender, declaring literals using type assertions.

There is only a difference of one character in length between these two statements but the behaviour in terms of type checking is very different. It is very common to see this as shorthand in places where you don't normally use an explicit declaration such as a return value of a lambda e.g.

When you start writing literals in this way in one place in your code, you'll fall into a habit of using this everywhere.

I think one of the main reasons why this is so prevalent is that the behaviour of the IDE when using a type assertion like this is very similar to declaring a value the "right" way. You get intellisense prompting you with the property names of the asserted type...

...and when you start typing, if you provide an incorrect property name, you get a compiler warning...

These things are all pointing to the type checking on this value being no different to that on a standard declaration - it lulls you into a false sense of security.

A note on version - this is true as of version 3.5.1.

I created a TypeScript playground with these examples in for you to follow along.

Object literals

In the example above, rather than defining a literal of a target (asserted) type what you're actually doing here is defining a literal of an implicit type which you then assert as the target type so it's the same as doing this...

For example, taking this trivial type with two required properties and one optional...

When processing the type assertion TypeScript simply checks that any properties on the source type with a name that matches one of the target type's properties also have the same type as that property. The first gotcha about this is that, if there are no properties on the source type, it matches even if the target type has required properties...

If properties are declared on the source you need to supply all the required properties of the target but after that any additional properties that may be present on the source type are disregarded unless they are a name match with a target property. The gotcha arising here being that, if you have a typo in the name of an optional property, you won't know about it.

In `foo5` and `foo6` the property `other` is incorrectly spelled `othen` but only the literal without the assertion catches this.

Primitives

For primitives type assertions error as you would expect for assigning a value of one type to a variable of a different type albeit with a much more long-winded error message.

Things are a bit more interesting with union types of constant values, though. For example, with a union of three constant string values the assertion allows an incorrect string value but it will fail if given a value of another underlying type e.g. a number.

Arrays

Arrays and type assertions don't agree at all. The assertion doesn't catch any wrong values added to the array...

Tuples

Tuples, however, are a different story and the assertion is a lot more strict with it essentially behaving the same as a standard declaration.

Summary

As mentioned in the TL;DR - don't mix literals with type assertions. It is fraught with type checking peril and the use of assertions should be restricted to only the cases where TypeScript genuinely has no information about a value.

For the shorthanded lambda return values just ensure you define the return type on the function type itself e.g.

For covariant return types writing the declarations out longhand is the safest option.

Up next time - why you shouldn't use parameters with your functions!

No comments: