Tianyi Song

There's no type casting in TypeScript

TypeScript felt very natural to someone who had learned programming in college with Java, just like me. It has the same-old beloved type security powered by compile-time type checking.

However, this sense of familiarity is exactly what tripped me up, in a recent project where we adopted TypeScript. What could go wrong?

An example

Consider this piece of code:

interface X {
    y: number
}

let obj = JSON.parse(`{"a": 1}`);
console.log(obj as X);

This program can successfully compile and run with no sign of error. But something is obviously wrong here: the parsed object obj doesn’t have the property y, so “casting” it to type X should fail.

Why?

The compiler doesn’t complain because TypeScript only checks types at compile time. All type information is erased by the compiler, and not available at run time (since TypeScript is a language extension to JavaScript and it transpiles to JavaScript).

In the example above, the compiler has no way to know what type obj actually is at run time. It can only rely on the programmer for the type. Here, we use the type assertion (not type casting) syntax and tell the compiler that we are sure that obj has type X, so that the rest of the code can type check based on this assumption.

This also applies to other type assertion syntax in TypeScript, like:

let obj: X = JSON.parse(`{"a": 1}`);

This counters my intuition about typed programming languages. In Java, casting an object with the wrong class will throw a ClassCastException. In Golang, unmarshal-ing a JSON object to a struct may also fail. I guess this is because JS wants to have JSON as a first-class serialization format for JS objects; and TypeScript can’t really do much about it.

The Correct Way

The Rule of Thumb is: only use type assertions when you’re sure about it .

There is no good way to cast a JSON object to a TypeScript object, at least based on my research so far, other than explicitly writing out serialization or deserialization class methods that consume or produce the typed object. This article here discusses the correct way(s) pretty well.

Extra: How We Learned This The Hard Way

One of our school projects used Express.js running on TypeScript as the backend, with node-postgres talking to the database. TypeScript wasn’t around when pg first started, and it obviously wasn’t built with types in mind. We used the community-contributed @types/pg definition package, only to realized that it uses any very, very generously.

Of course, we wanted to leverage the type safety features, so we had something like:

interface Account {
    username: string,
    age: number
}

// `rows` have type `any`
const rows = db.query("select * from accounts;").rows; 

// does `person` have `Account` type????
const person: Account = rows[0]; 

res.send(person); // sends the object to the client

And we thought person will naturally be type-checked, with all the Account properties available. And God saw our confused looks when we found out that the server returned very different responses than we were expecting.