Type systems¶
Why it matters¶
Every language has a type system. The differences (compile-time vs runtime, nominal vs structural, sound vs unsound, with or without generics) shape how code is structured and what categories of bugs the compiler can catch for you.
Read at least two paths' chapters on their type machinery. The contrast reveals why one library's API looks the way it does versus another's.
The lens, per path¶
Java - nominal, sound, gradually modernizing¶
Month 1 - Language & Toolchain (records, sealed classes), Month 4 (generics interactions). Classes form a nominal hierarchy (Animal and Dog extends Animal are explicit). Interfaces define contracts. Generics (since Java 5) with type erasure (runtime sees raw types). Modern additions: records (Java 14+), sealed classes + pattern matching (Java 17/21) bring algebraic data types.
What's unique: the slow-but-deliberate evolution. Java's type system has accumulated 25 years of additions while maintaining backward compatibility. Generics with erasure is the canonical example - what looks like first-class generics is implementation-erased at runtime; that's why you can't have List<int> (use List<Integer>), and why reflection on List<String> and List<Integer> returns the same erased type.
Strength: mature ecosystem; well-understood. Weakness: erasure complicates some generic patterns; null is still in the type system.
Rust - algebraic data types, ownership, traits, no null¶
Month 2 - Type System. Structs, enums-as-sum-types (each variant carries different data), traits (interfaces with default methods, no inheritance), generics with monomorphization (each instantiation becomes a separate compiled function), lifetimes (tracking how long borrowed references live). Option<T> replaces null.
What's unique: the type system encodes memory safety. Lifetimes prevent dangling references; Send/Sync marker traits prevent data races; exhaustive match over sealed enums prevents missed cases. Most concurrency bugs and many memory bugs are compile errors.
Strength: strongest correctness guarantees of any mainstream language. Weakness: the learning curve is real; borrowing + lifetimes + generics + async lifetimes compound.
Go - structural interfaces, generics (recent), unsafe escape hatch¶
Month 1 - Runtime Foundations for interfaces; Month 4 for generics (added in Go 1.18, late 2022). Interfaces are structural - io.Reader is "anything with a Read([]byte) (int, error) method"; no implements keyword. Generics with monomorphization but pragmatically limited (no method-on-type-parameter dispatch).
What's unique: structural typing is rare in mainstream languages (Go and TypeScript are the famous examples). It removes a layer of type-system ceremony: any type satisfies an interface without explicit declaration. Type assertions (if reader, ok := x.(io.Reader); ok { ... }) bridge typed and dynamic worlds.
Strength: simple to learn; small surface area. Weakness: the type system is less expressive than Rust's or Haskell's; some patterns (variance, higher-kinded types) aren't expressible.
Python - dynamic types + optional type hints (PEP 484+)¶
Month 1 - Foundations, Month 2 - Intermediate Idioms. Python is dynamically typed: types are tracked at runtime; the interpreter doesn't check before execution.
Type hints (since Python 3.5, PEP 484, with refinements through PEP 695 / 3.12) let you annotate. Tools (mypy, pyright, ty) check them statically. The runtime ignores them.
Modern Python: protocols (PEP 544, structural typing like Go's interfaces), generics (list[int], dict[str, int]), @dataclass for typed records, TypedDict for dict structures.
What's unique: the dynamic+optional combination. New projects often start dynamic, add types as they grow. Tools like pydantic use types at runtime for validation (the only "real" use of type hints at runtime).
Strength: flexibility for prototyping; the type-hint system is rich. Weakness: dynamic typing produces a class of runtime bugs strict type systems catch at compile time. Type-checking adoption is uneven.
C / C++ - the lineage¶
Not a path on this site, but the family tree's root for Go/Rust/Java type machinery. C has structs and pointers - barely a type system. C++ templates (compile-time generics with monomorphization) influenced Rust's generics. C++20 concepts introduced bounded generics. Worth knowing the heritage if you ever read older systems code.
Cross-cutting concepts¶
Structural vs nominal typing¶
- Nominal (Java, Rust, C++): types are equal because they have the same name.
Dog implements Animalbecause you wrote that. - Structural (Go, TypeScript, Python protocols): types are equal because they have the same shape. Any type with a
Bark()method satisfies an interface requiringBark().
Structural is more flexible (no explicit declaration); nominal is more explicit (you know exactly what implements what).
Generics - three implementation strategies¶
| Strategy | Used by | Behavior |
|---|---|---|
| Monomorphization | Rust, C++, Go (after 1.18) | Compiler generates a specialized function per type. Fast runtime, larger binary. |
| Type erasure | Java, TypeScript | One generic function at runtime, types erased. Smaller binary, requires boxing of primitives. |
| Runtime types | Python, Ruby, JavaScript | No real generics - duck typing throughout. |
The Rust/Go approach is faster but bloats binaries. The Java approach keeps binaries small but has the autoboxing problem and breaks naive reflection.
Variance¶
When Cat extends Animal, is List<Cat> a subtype of List<Animal>? The answer (covariant, contravariant, invariant) depends on the language and how you wrote the type.
- Java: wildcards (
List<? extends Animal>,List<? super Cat>) - the famous PECS rule. - Rust: variance inferred from usage; mostly invisible.
- Go: doesn't really arise - no inheritance among interface types.
Most beginners don't think about variance; reading the rules helps when a confusing compiler error mentions it.
Sound vs unsound¶
A sound type system: if the type checker accepts, no type errors at runtime. Unsound: holes exist.
- Rust: sound modulo
unsafeblocks. - Java: mostly sound; some corner cases (array covariance, type erasure interactions).
- Go: mostly sound; type assertions and reflection are the escape hatches.
- TypeScript: intentionally unsound (
any,as). - Python type hints: intentionally not enforced at runtime; soundness is a tool-level concern.
Soundness matters when you're betting on the type system to catch a bug class. Rust's commitment to soundness is part of why it's used in security-critical infrastructure.
The contrasts that teach¶
| Aspect | Java | Rust | Go | Python |
|---|---|---|---|---|
| Nominal / structural | nominal | nominal (mostly) | structural (interfaces) | dynamic + structural (protocols) |
| Generics | erasure | monomorphization | monomorphization (since 1.18) | runtime-only (list[int] is just a hint) |
| Sum types | sealed + records (modern) | enums (built-in) | unions via interfaces (awkward) | Union[A, B] / A \| B (hints only) |
| Null | in type system (null) |
not in type system (Option<T>) |
in type system (nil) |
in type system (None) |
| Inheritance | yes (single + interfaces) | no (traits + composition) | no (struct embedding, interfaces) | yes (multiple) |
| Compile-time enforcement | yes (modulo erasure) | yes (strongest) | yes (basic) | optional via tools |
The single most clarifying read: Java's sealed + records + pattern matching (Month 1) alongside Rust's enum + match (Month 2). Both implement algebraic data types - the same idea - using very different syntax. Once you see they're the same idea, every other ADT-shaped feature in any language reads easier.
What to read first¶
- You want strongest correctness guarantees → Rust Month 2. The type-system page is the feature.
- You write Java daily → Java Month 1 (records + sealed + pattern matching). Modern Java's most productivity-changing additions.
- You write Go → Go Month 1's interface section. Then Month 4's generics - recent and still maturing.
- You write Python and want fewer runtime bugs → Python Month 2. Adopt type hints +
mypy. The investment pays back fast. - You want one unifying read → study algebraic data types (enums with data) across Rust and modern Java side by side. The pattern then makes sense everywhere.