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 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.
"5" + 1 === "51").Most modern languages are strong-ish. Weak typing introduces classes of bugs (silent miscoercion) that are nearly absent in strong languages.
class Person {...} and class User {...} with same fields are different types.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.
Add type annotations to a dynamically-typed language. Untyped code coexists with typed.
any, as); the practical value of types is huge regardless.Gradual typing's value: incremental adoption, type benefits without full rewrite. Cost: type system limited by what's expressible without breaking dynamic semantics.
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.
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.
A way to say "any type that has these methods." Polymorphism without inheritance.
impl Trait for Type.Trait-based polymorphism is generally cleaner than inheritance-based. Modern OO languages (Kotlin, Swift) increasingly favour traits / protocols over deep class hierarchies.
The "null reference" was called "the billion-dollar mistake" by its inventor (Tony Hoare). Modern languages address it:
String vs String?), Swift (String vs String?), Rust (Option<T>), TypeScript (with strict null checks).Optional and @NonNull annotations; still non-default), Go (zero values; no nullability for compound types).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.
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.
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.
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.
The sweet spot for correctness-sensitive work.
When you want safety without exotic features.
For prototyping, scripting, data work.
For correctness through different means.
For practical impact:
instanceof.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.
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.