Contents

TypeScript: Understanding structural typing

Duck and structural typing

When we talk about duck typing or structural typing we are talking about the compatibility that different types may or may not have in a given programming language.

JavaScript is a duck typed language. What does it mean?

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

Following that precept, it means that rather than try to determine whether an object can be used for a purpose according to its type, in duck typing, an object’s suitability is determined by the presence of certain properties.

TypeScript models this behavior and bases its types compatibility on structural typing, in contrast to nominal typing.

Structural typing makes TypeScript not as strict as nominally typed languages like C# or Java and this sometimes can lead to confusion or unexpected results.

Having a good understanding of structural typing can help you make sense of errors and non-errors and help you write more robust code.

Understanding structural typing

The basic rule for TypeScript’s structural type system is that type A is compatible with type B if B has at least the same members as A.

Example

Say we have 2 different interfaces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface Person {
    name: string;
    age: number,
}

interface Alien {
    name: string;
    age: number,
    planet: string;
}

Structural typing allows to assign an object of type Alien to Person but not the other way around because Person lacks of the property planet.

Success
1
2
3
4
5
6
7
const alien: Alien = {
    name: 'ET',
    age: 5,
    planet: 'Mars'
}

const person: Person = alien
Error: Property 'planet' is missing in type 'Person' but required in type 'Alien'
1
2
3
4
5
6
const person: Person = {
    name: 'ET',
    age: 5
}

const alien: Alien = person // Error

This might look simple to deal with but it can sometimes lead to confusing results.

Say you want to create a function that prints the details of Person.

Error

You might think that something like the following would do the job

1
2
3
4
5
function print(person: Person) {
    for (const property in person) {
        console.log(`${property}: ${person[property]}`);
    }
}

But surprisingly person[property] will produce the following error:

Element implicitly has an ‘any’ type because expression of type ‘string’ can’t be used to index type ‘Person’.

The logic in the previous example assumes that the Person passed to the function is a “sealed” (or “precise”) type and therefore every property of the object would exist.

But as we saw earlier that is not the case. Like it or not types in TypeScript are “open” and they might contain more properties than the ones declared.

According to structural typing I could pass an Alien and the type checker wouldn’t complain about it. The problem is when we iterate over the properties of Person and because it might have to deal with unknown properties like planet, the type checker gives us that error.

If you are after a solution to this problem have a look to this article: How to iterate over object properties.

Benefits of structural typing

Structural typing is not always bad and in some circumstances it can be beneficial, for instance when you are writing tests.

Example

Say you have a function that returns the users of your application and process the results.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface User {
    email: string;
    firstName: string;
    lastName: string;
    fullName: string;
}

function getUsers(repository: Repository): User[] {
    const users = repository.findUsers();
    return users.map(x => ({ id: x.id, ...x }))
}

In order to test getUsers you could mock Repository but a better approach would be leveraging structural typing and create a narrower interface:

1
2
3
4
5
6
7
8
interface MyRepository {
    findUsers: () => any[];
}

function getUsers(repository: MyRepository): User[] {
    const users = db.findUsers();
    return users.map(x => ({ fullName: `${x.firstName} ${x.lastName}`, ...x }))
}

You can still pass getUsers a Repository since it has a findUsers method and because of structural typing it doesn’t need to implement MyRepository.

But in your test you can now pass a simpler object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
test('getUsers', () => {
    const users = getUsers({
        findUsers: () => [{
            email: 'example@example.com',
            firstName: 'John',
            lastName: 'Smith'
        }]
    })

    expect(users[0].fullName).toEqual('John Smith')
})

Conclusion

TypeScript uses structural typing to model the duck typed nature of JavaScript. Types are not “sealed” and values assignable to your interfaces might have more properties than the ones listed in your type declaration.