Type Systems Comparison

A type system is the rules a language uses to classify values and prevent operations from being applied to incompatible types. Type systems vary widely in expressiveness, when they check, what they check, and how much they get in your way.

This page is the working comparison: features that matter, tradeoffs, and what choosing a language with a given type system actually costs and buys.

Static vs dynamic

Static catches more errors earlier; tooling (autocomplete, refactoring) is better. Dynamic is faster to prototype and more flexible.

The 2020s consensus: most production codebases benefit from static typing. JS → TypeScript, Python → typed Python (gradual), Ruby → still mostly dynamic but with Sorbet / RBS adoption. The gap has narrowed; gradual typing solutions provide static-like benefits in dynamic languages.

Strong vs weak

Most modern languages are strong-ish. Weak typing introduces classes of bugs (silent miscoercion) that are nearly absent in strong languages.

Structural vs nominal

Nominal: more discipline; clearer intent. Structural: more flexibility; "duck typing" at compile time.

Most languages are mostly nominal. TypeScript is famously structural. Go interfaces are structural; structs are nominal.

Gradual typing

Add type annotations to a dynamically-typed language. Untyped code coexists with typed.

Gradual typing's value: incremental adoption, type benefits without full rewrite. Cost: type system limited by what's expressible without breaking dynamic semantics.

Type-system features that matter

Generics / parametric polymorphism

List<T> instead of List (where T is unknown until use). Type-safe collections, functions that work on any element type.

Every modern statically-typed language has them. Go held out until 1.18 (2022); now has them. Lack of generics in pre-1.18 Go was a notable deficiency.

Sum types / tagged unions / algebraic data types

A type that's "either A or B or C," and the compiler forces you to handle each case.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

match result {
    Ok(value) => ...,
    Err(error) => ...,
}

Eliminates entire bug categories: forgotten error cases, null-pointer-when-it-should-be-Some.

Languages with: Rust, Haskell, OCaml, Swift, Scala, Kotlin (sealed classes), TypeScript (discriminated unions), Python (with TypedDict and Literal — limited).

Languages without: Java (until pattern matching landed), Go, classic dynamic languages.

This is one of the highest-impact type features for code quality. Lacking it forces error-handling via exceptions, returning null/None, or convention.

Traits / interfaces / type classes

A way to say "any type that has these methods." Polymorphism without inheritance.

Trait-based polymorphism is generally cleaner than inheritance-based. Modern OO languages (Kotlin, Swift) increasingly favour traits / protocols over deep class hierarchies.

Null safety

The "null reference" was called "the billion-dollar mistake" by its inventor (Tony Hoare). Modern languages address it:

Non-null-by-default catches a huge class of bugs at compile time. The pain of being forced to handle nullability is dramatically less than the pain of NullPointerExceptions in production.

Pattern matching

Decompose a value while binding variables. Exhaustiveness checked.

match user {
    User::Admin { permissions } => grant_admin_access(permissions),
    User::Regular { email } if is_verified(email) => grant_user_access(),
    User::Regular { .. } => deny(),
}

Most useful when combined with sum types. Java has pattern matching as of 21+; Python 3.10+ has match statements.

Dependent types

Types that depend on values. Vec<T, n> where n is the length, known at compile time.

Languages: Idris, Coq, Lean (proof assistants); Rust (sort of, via const generics); F* (research).

Powerful for proving program properties; rarely used in industry. The marginal benefit doesn't justify the productivity cost for most engineering work.

Type inference

The compiler figures out the type without explicit annotation.

const x = 42;  // x is number, no annotation needed

All modern statically-typed languages have local type inference. Some go further:

Type inference makes static typing less verbose. Without it, statically-typed code is annotation-heavy and feels like punishment.

Choosing a language by type system

Strong static + strict null + sum types + good inference

The sweet spot for correctness-sensitive work.

Strong static, simpler model

When you want safety without exotic features.

Dynamic with optional types

For prototyping, scripting, data work.

Functional purity

For correctness through different means.

What's worth your time

For practical impact:

The trend: mainstream languages are slowly absorbing what was specialty 10 years ago — null safety, sum types, pattern matching, traits. Java in 2026 looks more like Scala in 2016.

What about runtime types vs static types

Runtime introspection (Python's isinstance, JavaScript's typeof, Java reflection) lets code make decisions based on type at runtime. Useful for serialisation, generic frameworks, dependency injection.

Modern static type systems (Rust, Haskell) intentionally have weak runtime introspection — the runtime doesn't know the type, because the type was a compile-time concept. Erased types: less flexible but more efficient and predictable.

Different trade-off. For framework-heavy code (Rails, Spring), runtime types help. For systems code, erased types help.

Further reading