Type Systems: Structural vs Nominal
Type Systems
Introduction
In this lesson, you'll learn about type systems: structural vs nominal in Java. Coming from TypeScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In TypeScript, you're familiar with type systems.
Java has its own approach to type systems, which we'll explore step by step.
The Java Way
Let's see how Java handles this concept. Here's a typical example:
// Java uses NOMINAL typing
interface Printable {
void print();
}
// Must EXPLICITLY declare that a class implements the interface
class Document implements Printable {
@Override
public void print() {
System.out.println("printing...");
}
}
public class Main {
static void output(Printable item) {
item.print();
}
public static void main(String[] args) {
output(new Document()); // Explicit implementation required
}
}Comparing to TypeScript
Here's how you might have written similar code in TypeScript:
// TypeScript uses STRUCTURAL typing
interface Printable {
print(): void;
}
// Any object with a print() method satisfies Printable
const doc = {
print() { console.log("printing..."); }
};
function output(item: Printable) {
item.print();
}
output(doc); // Works — shape matches, no explicit declaration neededYou may be used to different syntax or behavior.
TypeScript: structural — if the shape matches, the type matches
You may be used to different syntax or behavior.
Java: nominal — must explicitly declare implements InterfaceName
You may be used to different syntax or behavior.
Java interfaces are contracts enforced at the class declaration level
You may be used to different syntax or behavior.
No 'duck typing' in Java — the compiler checks the declaration, not just the shape
Step-by-Step Breakdown
1. Structural vs Nominal — The Core Difference
In TypeScript, a value satisfies a type if it has the right shape. In Java, a class must explicitly declare which interfaces it implements.
interface Flyable { fly(): void; }
const obj = { fly() {} };
const f: Flyable = obj; // OK in TS// Java — must be explicit:
class Bird implements Flyable {
public void fly() {}
}2. Functional Interfaces and Lambdas
Java interfaces with a single abstract method (SAM) can be implemented with a lambda — this is the closest Java gets to structural typing.
const onClick: () => void = () => console.log("click");// Runnable is a @FunctionalInterface
Runnable onClick = () -> System.out.println("click");3. Generics Are Nominal Too
Java generics are checked nominally. List<String> and List<Integer> are distinct types, and you cannot assign one to the other without wildcards.
function first<T>(arr: T[]): T { return arr[0]; }public static <T> T first(List<T> list) {
return list.get(0);
}4. @Override Annotation
@Override tells the compiler you are implementing or overriding an existing method. It is optional but strongly recommended — the compiler will error if the method signature doesn't match.
// TypeScript just matches the method name and signature@Override
public void print() { ... }Common Mistakes
When coming from TypeScript, developers often make these mistakes:
- TypeScript: structural — if the shape matches, the type matches
- Java: nominal — must explicitly declare implements InterfaceName
- Java interfaces are contracts enforced at the class declaration level
Key Takeaways
- TypeScript is structurally typed — shape determines compatibility
- Java is nominally typed — classes must explicitly declare implements
- Functional interfaces allow lambda syntax for single-method interfaces
- @Override annotation is best practice for all interface implementations