A key thing about the Typescript Type System is that most of the times it just works, but sometimes we get some surprising error messages that give us an indication that there is something fundamental about it that we might not be aware yet.

The great thing about Typescript, is that we could go for months using it without knowing some important concepts about what is going on with the type system.

We will find unexpected compiler error messages, but not to the point where we can't use Typescript and be productive with the language, because in general it just works.

But if we add these concepts to our toolbox, this will make our experience of the language much more enjoyable and productive.

We are going to break this down step by step into 3 key concepts.

A Simple Example - Why Doesn't This Work?

Let me give you a quick example of what we mean when we say that the type system is actually quite different than other type systems. Let's try to guess if this simple code example would compile our not:

If we are not familiar with how the Typescript Type system works, we might be surprised to realize that this actually does not compile. So why is that? Let's have a look at the error message we get:

Error:(54, 6) TS2339:Property 'name' does not exist on type '{}'.

So what is going here? We have defined an empty object first and then tried to assign it the name property. This is just plain Javascript code, and Typescript should allow us to write that transparently. So why the error message?

This is related to the first key concept that we are going to cover: Type Inference.

Key Concept 1 - Type Inference is Always On

The key thing to start understanding this error is to realize that Type inference is active here. So the user variable was automatically assigned a type even if we didn't add any explicit type annotation.

If we hover over the user variable, we can see the inferred type. In Webstorm, if we click the variable and hit Ctrl+Shift+P, we get the following inferred type:

type: {}

You might think at this point, what is this type?

You might have heard of the type Any and the compiler property noImplicitAny . If not have a look at this previous blog post Typescript 2 Type Definitions Crash Course - Types and Npm, how are they linked? @types, Compiler Opt-In Types: When To Use Each and Why ?.

We can see that the Any type is not related to this situation, because the inferred type is not Any. So what is that type that was just inferred?

We are going to understand it by providing another example, have a look at this and try to guess if it compiles, and if not what is the error message:

Again it might be a bit surprising that this code does not compile. Here is the error message:

Error:(59, 8) TS2339:Property 'lessonCount' does not exist on type '{ name: string; }'.

And if we check what is the inferred type of the variable course, we get this type:

type: {name:string}

Let's break this down, so what is going on in this scenario?

We can see that the variable course is not of type Any, it got a different type assigned

is not of type Any, it got a different type assigned The type inferred looks like it's the one of an object that has only one property named `name'?

We can set new values to this property called name

but we cannot assign any other variable to a variable of this type

Let's test this to see if its true

Let's see if this could be the case, that indeed a type was inferred with only one property. Let's define such type explicitly:

As we can see, we have defined the type inline using a Type annotation. The result is that we get the same error message as above: we can overwrite name but we cannot set a new property lessonsCount .

This seems to confirm that there was a type inferred with only one property. What if we define this type not inline, but create a custom type? For example like this:

In this case, we also get the same error message, which in this scenario would be less surprising because we are defining the type explicitly and not via type inference.

So what does this all mean? This leads us to the Key Concept number 2 in the Typescript Type System.

Key Concept 2 - Types are defined by the collection of their properties

In the current version of Typescript, the type system is said to be based on structural subtyping. What does this mean?

It means that what defines a type is not so much its name (like nominal type systems that are common in other languages). Instead what defines a type is a collection of the properties and their types.

For example what defines the type of the Course custom type is its list of properties, not its name. Also if an object has no type annotation, Typescript will look into its collection of properties and infer a type on the fly which contains those particular properties.

So how does this explain the compiler errors?

That is why the type inferred in course is type: {name:string} . Because the object only has that property. And this is also why we get a compiler error while assigning lessonCount to the course object.

This is also why we can't assign a name to the user property: because the inferred type is type {} , which means that user is an object with no properties, an empty object.

So we could only assign it to another empty object. And this leads us to the last key concept. Type Compatibility.

Key Concept 3 - Type compatibility depends on the list of properties of a type

As we have seen what really defines a type in Typescript is its list of properties, so that is also what defines if two types are compatible. Have a look at this example where we define two types and assign them:

There is still a compilation error here. This line named = course does compile correctly, because Course has all the mandatory properties needed by Name , so this type assignment is valid.

Note that the Course interface does not need to extend Named , like in other type systems (nominal type systems).

But the line course = named does not compile, and we get the following error:

Error:(73, 1) TS2322:Type 'Named' is not assignable to type 'Course'. Property 'lessonCount' is missing in type 'Named'.

So as we can see in the error message, the two types are not compatible because of a missing property, and not because the two types are different.

So How to we fix the compilation error?

Before going to the conclusions, let's go back to the initial example and make it compile, as it's a very common case:

By assigning the type Any to the user variable, we can now assign it any property we need, because that is how the Any type works. Another thing about the Any type is that we could take the variable user and also assign it to anything.

So annotating a variable with type Any is essentially telling the compiler to bypass the type system, and in general not check type compatibility for this variable.

How To Define Optional Variables

Another way of fixing this type of errors is to mark variables as optional, for example by annotating variables with a question mark:

In this example, we have marked the lessonCount variable as optional by adding a question mark to the member variable declaration in Course . So now the line course = named also compiles, because named has all the mandatory properties of the Course custom type.

Conclusions

The type inference mechanism and the type compatibility features of Typescript are very powerful and generally just work. We could actually code for a long time in Typescript without realizing what is going on under the hood except for some occasional error messages.

Why does the Type system work like this ?

We can see why the Type system is built like this: its to allow as much as possible a style of coding that is almost identical to plain Javascript.

Everything is based on type inference as much as possible, although there are places like function arguments where we need to add type annotation if setting noImplicityAny to true, because there is no way for the compiler to infer those types.

The type system is built in a way that most of the error messages we get are actually errors that we would want to fix.

What is the tradeoff involved ?

But there is a small tradeoff involved to get all these type safety features which include: catching errors at compile time, refactoring and find usages.

We will on occasion get an error for something that would work in plain Javascript like the first scenario in this post.

This does not happen often, and when it happens it can be fixed using the Any type. But its better to try to use Any the least possible, to keep all the benefits of the type system.

The Typescript language is continuously evolving, its even in the works the possibility of adding nominal typing, have a look at this Github issue.