Structural vs. Nominal Typing
Type Systems Deep Dive
How Do We Decide if Two Types are Compatible?
In a typed programming language, the type system needs a way to determine if a variable of type A
can be used where a variable of type B
is expected. For example, can you pass a Dog
object to a function that expects an Animal
?
There are two main systems for determining this compatibility: Nominal Typing and Structural Typing. Understanding this distinction is key to understanding why certain code is valid in a language like TypeScript or Go, but would be an error in Java or C#.
1. Nominal Typing ("Typing by Name")
In a nominal type system, compatibility between types is based on their explicit names and declarations. Two types are compatible only if they have the same name, or if one is explicitly declared to be a subtype of the other (e.g., through inheritance).
- Core Idea: "If it's not declared to be a
Duck
, it's not aDuck
." - How it works: The compiler looks at the names of the classes or interfaces.
class Dog
andclass Cat
are never compatible, even if they happen to have the exact same properties and methods. However, ifDog
is declared toextend Animal
, then aDog
can be used where anAnimal
is expected. - Languages: Java, C#, C++, Swift, Rust.
Example: Nominal Typing in Java
// We define two classes with the exact same structure.
class Person {
String name;
void greet() {
System.out.println("Hello, I am " + name);
}
}
class Employee {
String name;
void greet() {
System.out.println("Hello, I am " + name);
}
}
public class Main {
// This function explicitly requires a 'Person' object.
static void welcome(Person p) {
p.greet();
}
public static void main(String[] args) {
Person person = new Person();
person.name = "Alice";
welcome(person); // This is OK.
Employee employee = new Employee();
employee.name = "Bob";
// This is a COMPILE-TIME ERROR, even though Employee has the same shape as Person.
// In a nominal system, the names don't match, so the types are incompatible.
// welcome(employee); // Error: welcome(Person) cannot be applied to (Employee)
}
}
Explanation: Even though Person
and Employee
are structurally identical, the Java compiler rejects welcome(employee)
because the name Employee
is not the same as the name Person
. To make this work, Employee
would have to explicitly extend Person
.
2. Structural Typing ("Typing by Shape")
In a structural type system, compatibility between types is based on their shape or structure. If two types have the same structure—meaning they have the same set of properties and methods with compatible types—then the types are considered compatible.
This is often called "duck typing", from the phrase: "If it walks like a duck and it quacks like a duck, then it must be a duck."
- Core Idea: "I don't care what you're called, I only care about what you can do."
- How it works: The compiler checks if the type provided has at least all the properties and methods required. The names of the types are irrelevant.
- Languages: TypeScript, Go, Elm.
Example: Structural Typing in TypeScript
// We define two classes with the same structure.
class Person {
name: string;
greet(): void {
console.log(`Hello, I am ${this.name}`);
}
}
class Employee {
name: string;
greet(): void {
console.log(`Hello, I am ${this.name}`);
}
}
// This function expects an object with a 'name' (string) and a 'greet' (function).
function welcome(p: { name: string; greet: () => void }) {
p.greet();
}
let person = new Person();
person.name = "Alice";
welcome(person); // OK, Person has the required structure.
let employee = new Employee();
employee.name = "Bob";
// This is also OK! TypeScript sees that the 'Employee' class has the same
// shape as what the 'welcome' function expects, so it allows it.
welcome(employee);
// This would also work with a plain object literal
let celebrity = {
name: "Charlie",
age: 40, // Extra properties are fine
greet() { console.log(`Hi, I'm ${this.name}`); }
};
welcome(celebrity); // OK, it has the required 'name' and 'greet' properties.
Explanation: TypeScript doesn't care that Employee
isn't named Person
. It only cares that Employee
has a name: string
property and a greet(): void
method, which satisfies the "contract" required by the welcome
function.
Comparison and Trade-offs
Feature | Nominal Typing | Structural Typing |
---|---|---|
Basis | Type names and explicit declarations (class A , interface B ). | Type structure or shape (properties and methods). |
Flexibility | Less flexible. Requires explicit relationships (e.g., implements , extends ). | More flexible. Allows for "ad-hoc" polymorphism. Types from different libraries can be compatible if they have the same shape. |
Safety | Arguably safer / more explicit. Type relationships are intentional. You can't accidentally satisfy an interface. | Can be less safe. You might accidentally implement an interface just by having methods with the same name, even if the intent is different. |
Refactoring | Easier to refactor. Renaming a class will be caught by the compiler everywhere it's used. | Harder to refactor. Renaming a type might not cause an error if another type still matches the old structure. |
Use Case | Good for large, stable codebases where relationships are well-defined and explicit intent is important (e.g., enterprise applications). | Good for codebases that need to integrate with third-party code or for rapid prototyping where flexibility is key. |
Languages | Java, C#, C++, Rust | TypeScript, Go, Elm |
Summary for Interviews
- Nominal Typing = Typing by Name. Two types are compatible only if their names are the same or if one explicitly inherits from the other. This is the system used in Java and C#. It's explicit and intentional.
- Structural Typing = Typing by Shape. Two types are compatible if they have the same structure (properties and methods). This is often called "duck typing" and is used by TypeScript and Go. It's more flexible and allows for implicit compatibility.
- The choice between them is a fundamental design decision in a programming language, trading off flexibility (Structural) against explicitness and intent (Nominal).
- A great way to demonstrate your understanding is to show how the same code would be valid in TypeScript but would cause a compile-time error in Java.