Demystifying a TypeScript quirk

Posted on February 01, 2020 in programming

I recently read Asana's blog post on TypeScript quirks and took particular interest in the first TypeScript quirk they mention. While it may seem like an inconsistency, the way the type system behaves here is entirely logical.

The article uses the interface Dog and function printDog as an example,

interface Dog {
  breed: string
} 

function printDog(dog: Dog) {
    console.log("Dog: " + dog.breed)
}

This code mimics a widespread pattern in TypeScript and acts as a base for typing of objects. So far, so good, but the article then introduces a case that could appear to be an inconsistency. While you can pass any object that contains a breed property to the function when it's assigned to a variable, you cannot pass it directly to the function.

const ginger = {
    breed: "Airedale",
    age: 3
};
printDog(ginger); //works

printDog({
    breed: "Airedale",
    age: 3
}); //fails

What's going on here? TypeScript checks for excessive properties in objects to catch bugs, but why does it only become an issue when the object is passed directly to the function?

As the article points out, TypeScript is a structurally typed language. This classification means that types are checked based on the object's structure rather than an inherent type on the object. There is an exception to this rule; however, which is when the developer explicitly types a variable. TypeScript functions parameters are also covariant, which means that they allow anything that extends the base type. In a structurally typed language, adding a property to an already known type would be seen as extending it.

So why does this error at all then? If it's ultimately allowed to pass in objects that are subtypes of the given type, why does it complain in some circumstances?

The answer comes back to the exception in the type system that I brought up earlier. When creating a variable and passing it to the function, the variable infers a type. When you create the object in the function call, you are explicitly telling TypeScript that the variable is of type Dog. Creating variables has an excessive type check, but function calls do not due to covariance. This "inconsistency" also has nothing to do with the function call. You can create the same behaviour just by providing a type when creating the object.

const ginger: Dog = {
    breed: "Airedale",
    age: 3
}; //fails

This code will provide the same error as the function call, as age is not a known property on the type Dog.

You can also do the inverse, and use generics to allow the function call to infer a type that extends the base type. If you change the function call to be the following, it will enable you to provide any arbitrary properties beyond the base required ones.

function printDog<T extends Dog>(dog: T) {
    console.log("Dog: " + dog.breed)
}

printDog({
    breed: "Airedale",
    age: 3
}); //works

While issues like this can often confuse TypeScript developers, there's usually an explanation for any behaviour that the type system exhibits. If not, you should probably report it to Microsoft.

Other Posts

The 4 year late postmortem of an Advanced Aimbot Detection system

Posted on August 06, 2016

Four years ago, I was hired by a gameserver to create an advanced server-side aimbot system. This is what I learnt.

Exceptions as Flow Control in Java

Posted on December 27, 2016

Exceptions are commonly used for flow control in Java, but how well do they perform compared to return values?

Optimizing data-heavy JavaScript code

Posted on March 01, 2020

JavaScript may not seem to be the ideal language to manipulate large amounts of data. This post goes over a few key problem areas and how you can avoid them.