If my grandma had wheels — Structural Typing in Typescript

Jordan Sorensen
Latch Engineering Blog
6 min readMar 22, 2021
“If my grandma had wheels, she would be a bicycle” -Typescript

Here at Latch, we leverage the power of strict type systems — our backend is written largely in Java and our web apps are built mostly using Typescript. The compile-time type guarantees of strict type systems give us greater certainty that our code is correct, type information serves as bare-bones documentation for even the mostly poorly documented code, and the shared paradigm makes switching between languages easier for anyone who wants to move around the stack.

Typescript has enjoyed widespread success, in large part by enabling developers to enjoy the benefits of strict type systems gradually, picking and choosing where they want to put in the extra effort to get those strong compile time guarantees. But Typescript’s differences from Java go beyond making strict typing optional. In fact, many developers won’t realize until deep into their Typescript education that Typescript is a bit of a black sheep of the strictly typed family. Most strictly typed languages — Java, C#, Swift, and many others — are nominally typed, while Typescript is one of just a handful of structurally typed languages.

What is structural typing?

Imagine you have a function which requires a Person object as input:

And now you call that function, passing in an object created on the fly:

Typescript developers have probably written similar code many times. “But hold on a minute,” Java developers might say. “That’s not a Person. That’s an Object that just happens to have the same properties as Person.”

And that’s exactly the idea behind structural typing — while nominal typing cares about the named type of a value, structural typing cares only about the structure of the value. In a structural type system, an object meets the requirements of a type if it has the same properties and methods with the same types as the requested type.

If it quacks like a duck…

Some developers may have heard this referred to as “duck typing” — if it walks like a duck and it quacks like a duck, then it’s a duck. Structural typing and duck typing are very similar — “structural typing” is often used to refer to such checks run at compile time, while “duck typing” refers to the same checks run at runtime.

Why?

What are the advantages of a structural typing system?

First, structural typing is a necessity for Typescript to be compatible with Javascript’s duck typing and is a key part of how Typescript allows types to be added gradually to an untyped code base.

Let’s imagine you’re starting with a Javascript version of the createPerson method above.

You’re migrating to Typescript and want to add type annotations so that anyone calling createPerson can rely on the compiler to ensure they’re calling it correctly.

In a nominal type system, any caller of createPerson must now create a Person object. At the very least, that means that adding types to createPerson will require changes to every place createPerson is called, and could require changes far from createPerson, tracing back to wherever that object originated. Adding a single type annotation could require changes far from where that annotation is found, making gradual addition of types difficult.

In a structural type system, my object just needs to have the right properties — the type I named when I created the object doesn’t matter. That means the compiler can still give me similar guarantees — the compiler can trace that object back to its origin and make sure it has the right properties. But because the compiler doesn’t care about what type I named when I created the object, I don’t have to make changes in those distant parts of the code where the object originated.

Second, structural typing removes some steps that are required in nominal type systems, making development more dynamic and fluid without completely giving up on compile-time checks. For example, consider the createPerson example that opened this article:

In this example, we declared the requirements of the createPerson method (it must be passed a Person), but we didn’t have to declare that the object we passed in met those requirements by declaring it as a Person — the compiler checked this for us.

We can skip not just the step of declaring the object, but of declaring a type at all, and STILL get the same compile-time checks. Imagine that we are not only creating a list of Persons, but also saving preferences for each Person after they’ve been created. We need to combine the id we get when we create the Person with the preferences we want to save for that Person:

Here, structural typing allows us to quickly throw together a new type that combines an id with a set of preferences to save. Because this type is only ever used here, in this Person creation pipeline, it doesn’t make sense to declare a whole new type for this particular combination of data. But the compiler will still check that the correct data is present — if the output type of createPerson changed, or the input type of savePreferences changed, the compiler would complain. In a nominal type system, we could get similar compiler guarantees by declaring a new type:

With structural types, we get all the same guarantees without ever needing to declare this intermediate type.

Emulating structural typing

These benefits are compelling enough that other languages have a number of features to enable similar benefits to structural typing.

Java and C# developers make extensive use of interfaces to declare “My object has the following capabilities.” But because they are nominally typed, a class must declare that it implements an interface when the class is declared. It’s common in Java to see an interface like Runnable which declares a single method run(), essentially saying “my type is a thing that can be run”. But with a nominal type system, the class must declare itself to be Runnable, while in a structural type system, anything with a run() method could be used where a Runnable is required.

Swift comes even closer — a Swift Protocol is similar to a Java Interface, but Swift also supports Extensions, which let the developer declare that a type conforms to a Protocol without modifying the original type definition. Swift even supports Protocol composition so that — like Typescript — your API can declare that it requires an input with capabilities A and B.

But neither of these offer the benefits of having the compiler check requirements that the developer never declared — it is still up to the developer to define any type that they would like the compiler to check.

Pitfalls of structural typing

Of course, structural typing is not without its downsides. Although property names and types are often enough to identify the meaning of a property, it’s easy to imagine counterexamples — although a Person and a Process can both run(), they probably mean two very different things and it would be a mistake to ask a Process to run a 5k.

In fact, there are times we use nominal typing very deliberately. For example, it’s common to declare types like PersonId and LockId which under the hood are really just numbers. Having two different types (in a nominal type system) would help protect the developer from accidentally passing a LockId into the getPerson(id: PersonId) method. With a structural type system, however, the compiler sees that they are both numbers and happily accepts either.

There are certainly workarounds for cases like these — declaring private properties with special values that are unlikely to be accidentally duplicated elsewhere — but they’re far from elegant.

Unique type system for a unique language

Typescript has managed to strike a difficult balance — easy adoption by developers and code bases used to the freedom of Javascript, but maintaining much of the power of strict type systems. Its adoption of a unique “structural type system” was a key part of this balance, enabling developers to more quickly and easily express the requirements of their APIs, and to more easily start using those APIs. It’s just one more reason that we at Latch have found Typescript such a useful tool to build reliable, maintainable software systems.

--

--