What is the structural type system?

Nominal typing

In programming, data type names, especially in languages like Java and C#, carry significant weight. Nominal typing, the system employed by these languages, emphasizes the importance of type names. Even if two classes share identical properties, they are considered distinct and incompatible if they have different names and lack shared inheritance or interface. Compatibility requires classes to be related through inheritance or a shared interface, even if they seem identical.

As an example, in Java, if we have a class Person with properties like name and age, and another class User with the same properties but no direct inheritance or interface, we cannot use a Person object where a User is expected. In these languages, type names carry substantial weight.

Unlike nominal typing, TypeScript, a superset of JavaScript, employs “structural typing.” It prioritizes type structure over names—types with matching structures are treated as interchangeable, regardless of their names. This “structural typing” approach enhances dynamic flexibility in working with types, challenging conventional principles.

Note: This structural typing concept also applies to inheritance. In many languages, if we have a class Child that inherits from Parent, we can only use a Child where we expect a Parent. But in TypeScript, if a Child has all the properties and structure of a Parent, TypeScript treats them as compatible, even if there's no explicit "extends" relationship between them. This can be a bit surprising if you're used to languages where type names and inheritance hierarchies are strict. But in TypeScript, it offers a more flexible and dynamic way of working with types.

Structure typing in TypeScript

As we have discussed, in TypeScript, the crux of structural typing is the structure or shape of a type, not its name. A type's structure is defined by its properties and their corresponding types. Therefore, if two types share the same structure, TypeScript considers them interchangeable.

Let's illustrate this with an example:

type Person = { name: string; age: number };
type User = { name: string; age: number };

Even though these two types bear different names (Person and User), TypeScript treats them as the same type because their structures (with properties name and age) are identical. This means that a type is compatible with another if it has at least the same properties with matching types. This compatibility extends to variables, function parameters, and return values. For instance, if we have a function that expects a User, we can still pass a Person because their structures match.

type Person = { name: string; age: number };
type User = { name: string; age: number };
function printUser(user: User) {
console.log(user.name, user.age);
}
const person: Person = { name: "John Doe", age: 30 };
printUser(person); // This works because Person's structure matches User's

Code explanation

Lines 1–2: Two types are defined: Person and User. Each type is described as an object with two properties: name and age. Both properties have specific types—name is of type string, and age is of type number.

Lines 4–6: A function named printUser is declared. This function takes a single parameter named user, which is expected to be of type User. Inside the function, it logs the name and age properties of the user object to the console.

Line 8: A constant variable named person is defined and assigned an object. This object conforms to the Person type definition, with name set to "John Doe" and age set to 30.

Line 9: The printUser function is called with the person object as an argument, like this: printUser(person);. TypeScript checks if the person object (which is of type Person) can be used as an argument for the printUser function, which expects a parameter of type User. TypeScript determines that the person object's structure (properties name and age) matches the expected structure of a User. Therefore, TypeScript allows the call to proceed without any type errors and we get the output without any errors.

Structural typing vs. nominal typing

It's important to distinguish structural typing from nominal typing. In nominal typing systems, the names of types are crucial for determining type compatibility. Let's illustrate the difference between structural typing in TypeScript and nominal typing in Java with an example:

class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
class Main {
public static void greet(Person person) {
System.out.println("Hello, " + person.name + "!");
}
public static void main(String[] args) {
User user = new User("John Doe", 30);
greet(user); // This doesn't work in Java, because User and Person are distinct classes.
}
}

In Java, as demonstrated in the example, we cannot pass a User object to a method expecting a Person, even if the structures of the User and Person classes are identical. The only way to achieve compatibility in Java would be to create a common interface or superclass for User and Person and have them implement or extend it.

Conclusion

In conclusion, TypeScript's structural typing brings flexibility to type compatibility by emphasizing the structure of types rather than their names. This approach allows for more dynamic and adaptable code, making TypeScript a powerful tool for modern web development.


Free Resources

Copyright ©2025 Educative, Inc. All rights reserved