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.

What is the quirk?

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 objects. So far, so good. 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 is already 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 attached to it. There is an exception to this rule, however. When the developer explicitly types a variable when assigned, the object must strictly match that type. 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 extend 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 explicitly tell 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

Conclusion

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 the TypeScript team.

About the Author

Maddy Miller

Hi, I'm Maddy Miller, a Senior Software Engineer at Clipchamp at Microsoft. In my spare time I love writing articles, and I also develop the Minecraft mods WorldEdit, WorldGuard, and CraftBook. My opinions are my own and do not represent those of my employer in any capacity.