Skip to content

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.

def add(a: int, b: int) -> int:
    return a + b

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 Animal because 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 requiring Bark().

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 unsafe blocks.
  • 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.