Understand TypeScript’s Structural Type System

Juno Ng
CodeX
Published in
4 min readAug 23, 2021
Photo by Chor Tsang on Unsplash

TypeScript is a superset of JavaScript. On top of what JavaScript offers, TypeScript adds an optional static type system to it. The type system requires developers to define types for different parts of the program or it will automatically infer ones. The magic of TypeScript comes from its ability to catch bugs during compile time by spotting the inconsistency between the types declared and their usages. Understanding how the type checking mechanism works and its logic for assessing type compatibility is crucial to the design of program types which provide type safety. However, there are many typing systems which have different rules of typing checking. This article aims to introduce the system TypeScript uses and discuss the implications to type design.

Structural type system

How does TypeScript determine whether an object is compatible with a specific interface? In a strict sense, an object with its declaration implementing the interface and properties identical to the interface requirements can be safely claimed to be compatible with the interface. But more often we have objects that are not so perfectly matched, especially when object literals are common in JavaScript.

TypeScript adopts a structural type system which determines type compatibility and equivalence based on the type structure or definition rather than the declarative relationship between types and interfaces, which contrasts with nominal type system.

Let’s look at an example:

Function triggerWalk takes a parameter walker of type Walker and invokes its walk method. Since the interface of Walker requires only a walk method and variable person has it, person is compatible with the interface requirement of Walker and therefore TypeScript does not throw any error.

One must be aware that variable person has also a talk method, which makes it not identical with interface Walker . TypeScript’s structural type system accepts person so long as it has all properties the interface requires.

Also that the declaration of variable person does not involve keywords like implements or extends to interface Walker , and the variable itself is not declared to be of type Walker , how the variable is declared is not important to the type checking.

Structural typing is more flexible and has more concise syntax. However, it also means that relationships between types are more implicit and not as obvious as a nominal type system. The flexibility also makes catching certain of bugs difficult. As a result, TypeScript has added a process called excess property checking to it.

Excess property checking

Given the abovementioned, the following example might be confusing:

If the structural type system cares only whether the required properties are present, why would there be an error complaining the existence of talk method? The main difference between this and the previous example is that triggerWalk is invoked with an object literal rather than a variable to the object literal as the argument. This has triggered a process called excess property checking which throws errors on properties not stated in the interface.

Excess property checking is also triggered when you assign an object literal to a variable with type declared. It is added to TypeScript to limit the possible values of object literals and provide some safeguards.

To bypass this checking, you can create an intermediate variable and set it to the object literal. Since the type system is not checking an object literal directly now, excess property checking is not triggered and the error goes away.

Type design implications

As a structural type system determines types’ compatibility based solely on the properties they possess, this can create certain confusing results.

Take the following as an example:

Interface Goose possesses all the properties interface Duck requires so a goose is a duck too, at least from the type system’s perspective. To frame it more precisely, Goose is a subtype of Duck . While we know that both geese and ducks are birds, they are not the same kind of animal. It does not sound correct to say a goose is a duck.

One way we can correct this is to give the interfaces more descriptive names. Instead of taking a parameter of interface Duck , we can create an interface Walkable and type the parameter of it. Also, for the name of the function, instead of naming it walkDuck , we can give it a more generalized name walk . Now it makes better sense to say a goose is walkable and we have a walk function that accepts any walkable objects.

Another way we can cope with the problem is to add a tag to interface Duck . In the example below, a property called type is added to Duck . It acts like an identity check to other types. To be a subtype of Duck , we have to add to Goose the property of type equal to “duck”, which developers should not do. Compared with the former approach, the latter approach is more preventive in nature. It makes the type more verbose but easier to catch up for developers from nominal typing languages.

Conclusion

Understanding the structural typing mechanism of TypeScript is important. It helps developers to design more flexible and concise types. It can also be confusing to developers from nominal typing languages as the implicit relationships between types require a different mindset of type design.

--

--