Type Systems — Cross-Language Comparison

Type Systems — Cross-Language Comparison

  • Type: cross-cutting comparison
  • Languages covered: 51 (see _index)
  • Last updated: 2026-05-07

TL;DR

  • The “static vs dynamic” dichotomy is dead — the real spectrum is dynamic → gradual → static-nominal → static-structural → static-with-classes/traits → dependent, with several orthogonal axes (variance, inference power, totality, linearity).
  • Hindley-Milner (and its descendants ML, Haskell, OCaml, Roc, Gleam) gives you full inference + parametric polymorphism. Most modern type systems are HM with extensions bolted on.
  • Type classes / traits (Haskell, Rust, Scala, Swift) are the modern way to do ad-hoc polymorphism without subtype inheritance — they replace OO method dispatch with constraint-based dispatch.
  • Dependent types (Idris, Agda, Lean, Coq/Rocq) blur the boundary between types and values; types can compute, theorems can be proved. Cost: harder to write, slower to typecheck, fewer engineers.
  • Gradual typing (TypeScript, Hack, Dart strict) lets you migrate from dynamic to static incrementally — you pay nothing to start, and you opt into safety where you want it.
  • Set-theoretic types (Elixir, TS unions/intersections) treat types as sets and use set operations (∪, ∩, complement) as first-class type constructors.

The spectrum

DYNAMIC ──────────── GRADUAL ─────── STATIC NOMINAL ─── STATIC STRUCTURAL ── HIGHER-KINDED ── DEPENDENT
  |                      |                |                    |                  |              |
Bash/Tcl              TypeScript       Java/C#              Go (interfaces)     Haskell        Idris/Agda
Smalltalk             Python+mypy      Kotlin/Swift         OCaml records       Rust traits    Lean/Coq
Common Lisp           Ruby+RBS         C++ classes          Scala               Scala
Erlang/Elixir         Hack             Dart strict          F#                  Swift protocols
Perl/Lua/PHP          Elixir set-th    Crystal              TS structural       OCaml functors
Tcl/R                 Clojure spec     Pony                                     Common Lisp MOP
Prolog                                 Nim/Zig
                                       Odin/V/Roc/Gleam

Categories

1. Untyped (or “everything is a string”)

The interpreter does not distinguish types in the language; values carry tags but the language doesn’t have a checker.

  • Bash — strings only; type discipline is by convention.
  • Tcl — “everything is a string” historically; internal representations are typed but the language is untyped.

2. Dynamically typed — strict

Types are checked at runtime; mismatches throw immediately.

  • Python — strong + dynamic. Optional typing module + mypy/pyright/pyrefly. PEP 695 (3.12) added explicit type-parameter syntax; PEP 749 deferred annotations (3.14).
  • Ruby — strong + dynamic. RBS + Sorbet for gradual typing.
  • Perl — values have a “context” (scalar/list/boolean) more than a type.
  • R — dynamic; vectors are core, scalars are length-1 vectors.
  • Juliadynamic dispatch with an expressive type system — methods specialize by argument types via multiple dispatch; type annotations are optional and used for dispatch, not safety.
  • Lua — 8 fundamental types; tables are everything.
  • Smalltalk — dynamic; everything is an object; typing is conventional.
  • Prolog — terms are dynamically typed; mode declarations (Mercury) add static checking.

3. Dynamically typed — weak

Implicit conversions everywhere. Famous for surprise.

  • JavaScript== coercion infamy. Use ===. ES2020+ has nullish coalescing, optional chaining.
  • PHP — modern PHP (8.x) is dramatically less weak; type declarations + strict_types help.

4. Optional typing — gradual, retrofitted

Started dynamic; added an optional static layer. The static checker is non-mandatory.

  • Python + mypy/pyright — annotations are runtime-introspectable but not enforced by CPython. Strict mode possible.
  • Ruby + RBS/Sorbet — RBS files are sidecar; Sorbet inlines T.let/T.cast.
  • PHP — type declarations on params/returns/properties + declare(strict_types=1).
  • Clojure — clojure.spec for runtime contracts; core.typed (defunct) for static.
  • Common Lispdeclare + the give the compiler hints; SBCL emits warnings on mismatch.
  • Lua — Luau (Roblox) adds a static type checker on top.
  • Elixir — set-theoretic types in 1.18+ (gradual + structural; novel design by José Valim and Giuseppe Castagna).

5. Gradual typing — first-class

The language was designed gradual from the start; types are a primary feature.

  • TypeScript — gradual layered on JavaScript. Conditional/mapped/template-literal types. As of TS 6+ and the TS 7 Beta (“Corsa”, Go-based compiler) of 2026-04-21, performance is dramatically improved.
  • Dart — sound gradual: dart-strict checks fully; dynamic is opt-in.
  • Hack — Facebook’s PHP gradual fork (mention only; not in our 51).

6. Static — nominal, mostly inferred locally

Types are declared at boundaries; locals get inferred. Class identity matters (subtyping is by name, not shape).

  • Java — nominal classes + interfaces. Generics with use-site variance (? extends, ? super). Records + sealed (Java 17+); pattern matching maturing.
  • [[Languages/csharp|C#]] — nominal; covariant/contravariant generic parameters declared at definition. Records, init-only setters, primary constructors (C# 12+).
  • Kotlin — nominal; declaration-site variance (out, in); reified generics in inline functions.
  • Swift — nominal classes/structs; protocols (structural-ish — see category 8).
  • Go — nominal types but interface satisfaction is structural (no implements). Generics added in 1.18 (2022); type inference is intentionally limited.
  • Dart strict — nominal, sound subtyping.
  • Crystal — nominal + strong inference; union types pervasive.
  • Pony — nominal + structural interfaces; types carry reference capabilities.
  • C++ — nominal classes + concepts (C++20) for constraint-based generic dispatch.
  • Zig — nominal structs/unions/enums; comptime blurs the line at compile time.
  • Odin — nominal; where clauses constrain procedures.
  • V — nominal + interfaces.
  • Nim — nominal + concepts (structural constraints) + generics.
  • Ada — strongly nominal; subtypes have ranges/discriminants; protected types.
  • Pascal (Object Pascal) — nominal; interfaces are nominally typed but structurally compatible.
  • C — nominal but extremely weak; pointers can alias; void * escapes.

7. Static — Hindley-Milner / let-polymorphism

Full type inference; you almost never write a type. Polymorphism via parametric (generics) is the norm.

  • OCaml — the canonical HM. Module functors. GADTs since 4.x. First-class modules.
  • [[Languages/fsharp|F#]] — HM + .NET interop. Computation expressions, units of measure, SRTP.
  • Haskell — HM extended with type classes, kind polymorphism, type families, GADTs.
  • Roc — HM + abilities (typeclass-equivalent) + opaque types.
  • Gleam — HM + targets BEAM and JS. No higher-kinded types — intentional.
  • Elm — (mention; not in 51) HM with no typeclasses (intentional minimalism).

8. Static — type classes / traits / protocols

Ad-hoc polymorphism via constraints. Methods dispatched not on inheritance but on satisfaction of a constraint.

  • Haskell — type classes (the original). Higher-kinded; constraint kinds.
  • Rust — traits + associated types. Coherence enforced (orphan rules). No HKTs (workarounds via GAT).
  • Scala — Scala 3’s given/using is the modern face; was implicits in Scala 2. Type classes via implicit/given.
  • Swift — protocols + protocol extensions; primary associated types since Swift 5.7. Existential types (any P) and opaque types (some P).
  • Idris — interfaces (typeclass-equivalent).
  • Agda — instance arguments (typeclass-equivalent).
  • Lean — typeclasses with rich resolution.

9. Static — structural typing

Type identity is shape-based; if two types have the same fields/methods, they’re compatible.

  • TypeScript — purely structural (objects + functions match by shape).
  • Go — interfaces are structural; no explicit implements.
  • OCaml — object types are structural; module signatures are mostly nominal.
  • Scala — refinement types ({type T = X}) and structural types (slow at runtime due to reflection).
  • Pony — interfaces are structural; traits are nominal.

10. Static — variance polymorphism

Generic parameters can be marked covariant/contravariant.

  • Declaration-site variance: [[Languages/csharp|C#]] (out T, in T), Kotlin (out T, in T), Scala (+T, -T).
  • Use-site variance: Java (? extends, ? super).
  • Invariant by default: Rust, Go, TypeScript (with bivariance escape hatches).

11. Static — refinement / liquid types

Subtypes constrained by predicates: {x: Int | x > 0}. Decidable subset of dependent types.

  • Haskell (Liquid Haskell, plugin) — extends GHC with refinement types.
  • Ada / SPARK — subtype predicates + GNATprove for static verification.
  • Scala (community: refined library) — refinement DSL.

12. Static — linear / affine / quantitative

Track usage-count: 0 (erased), 1 (use exactly once), ω (unrestricted).

  • Idris (2) — Quantitative Type Theory. The flagship example.
  • Rust — affine (use-at-most-once) via the borrow checker. Closely related but no first-class linearity in types.
  • Haskell-XLinearTypes since GHC 9.0.
  • Agda0/1/ω quantities (Agda 2.6+).
  • Pony — reference capabilities are a uniqueness/sharing discipline; not “linear” but related.
  • Clean — uniqueness types (mention; not in 51).

13. Static — dependent types

Types can depend on values. The type Vec n (vector of length n) lets the compiler verify that head : (n > 0) → Vec n → A won’t run on empty vectors.

  • Idris (2) — full dependent types + QTT. Type-driven development workflow.
  • Agda — full dependent types. Cubical Agda for HoTT.
  • Lean (4) — dependent types + tactic-based proofs. mathlib4 is the largest formalized math library in any system.
  • Coq (Rocq) — dependent types via Calculus of Inductive Constructions; Ltac/Ltac2 for tactics. Used in CompCert, Four-Color theorem, Feit–Thompson.

14. Static — set-theoretic types

Types are sets; type operations (∪, ∩, complement) are first-class.

  • Elixir — set-theoretic gradual types (1.18+, Castagna et al.).
  • TypeScript — union/intersection types are similar in spirit (less rigorous theory).
  • CDuce — academic (not in 51).

15. SQL’s type system (its own thing)

Three-valued logic (true / false / unknown=NULL); each engine has its own type set.

  • SQL — the standard defines core scalar types (integer, varchar, date, etc.); each dialect extends. JSON is a 2023 addition. Type coercion rules are dialect-specific (Oracle empty-string-is-NULL is the famous trap).

Per-language quick reference

LanguageFamilyInferencePolymorphismNotable
PythonDynamic + gradualn/a (annotations)structural via duck typingmypy/pyright; PEP 695 syntax
JavaScriptDynamic, weakn/an/aES2025; coercion footguns
TypeScriptGradual structuralstrongparametric + structuralTS 7 Beta (Corsa, Go-based)
JavaStatic nominallocal (var)parametric + use-site varianceRecords, sealed, pattern matching
CStatic (weak)nonemacros onlyvoid *, undefined behavior
C++Static nominalstrongtemplates + concepts (C++20)SFINAE → concepts
[[Languages/csharp|C#]]Static nominallocal (var)parametric + decl-site varianceSource generators, records
GoStatic nominal+structuralweakparametric (1.18+)structural interfaces
RustStatic nominal + traitsstrongparametric + traitsAffine types; lifetimes; coherence
SwiftStatic nominal + protocolsstrongparametric + protocolsExistentials + opaque types
KotlinStatic nominalstrongparametric + decl-site varianceReified generics; null safety
RubyDynamic + gradualn/aduck typingRBS + Sorbet
PHPDynamic + gradualn/astructural duck typingstrict_types declare
ScalaStatic nominal + structural + traitsstrongparametric + given/usingScala 3 match types, opaque types
DartStatic gradual soundstrongparametricNull safety, mixins
ElixirSet-theoretic gradualweakpattern matchingCastagna et al. type system 1.18+
HaskellStatic HM + classesfullparametric + classes + HKTsGADTs, type families, -XLinearTypes
ClojureDynamic + gradual via specn/aprotocols + multimethodscore.async; spec
[[Languages/fsharp|F#]]Static HMfullparametric + classesSRTP; units of measure
LuaDynamicn/aduck typingLuau static layer
RDynamicn/aS4/R6 method dispatchNSE + tidy eval
JuliaDynamic + multiple dispatchoptionalmultiple dispatchtype system used for dispatch, not safety
ZigStatic nominalstrongcomptime polymorphismcomptime is the meta-system
NimStatic nominal + conceptsstrongparametric + conceptsmacros + templates
CrystalStatic nominalstrongunion types pervasiveunion types are inferred
OCamlStatic HMfullparametric + functors + classesFirst-class modules; GADTs
PerlDynamicn/aduck typingModern: native class (5.38+)
ErlangDynamicn/apattern matchingDialyzer success-typing
RacketDynamic; Typed Racket siblingn/adispatch via prefab structsTyped Racket has full HM
Common LispDynamic + declarativen/aCLOS + multimethodsdeclare/the for hints
SchemeDynamicn/adispatch on tagsR6RS optional types
FortranStatic nominalnoneparameterized derived types (PDT)Strong typing for arrays
COBOLStatic nominalnonen/aPICTURE clauses
AdaStatic nominal + SPARK refinementslocalgenerics + varianceSubtype predicates
PascalStatic nominalnone/limitedgenericsStrong typing tradition
PrologDynamicn/adispatch on termsMercury adds static + modes
TclUntyped (string-typed)n/an/aInternal types invisible
GroovyDynamic + @CompileStaticstrong (when static)duck or staticdynamic by default
BashUntypedn/an/astrings only
PowerShellDynamic + .NET typesstrong (typed mode).NET-flavoredType-constrained params
SQLStatic + 3VLn/an/aNULL = unknown; dialect-specific
VStatic nominalstronginterfaces + genericsSum types
OdinStatic nominalweakparametric + where constraintsNo subtype-OO
RocStatic HM + abilitiesfullparametric + abilities + opaqueTags (open/closed)
GleamStatic HMfullparametric onlyNo HKTs (intentional)
PonyStatic nominal + structural + refcapsstrongparametric + traits + interfaces6 reference capabilities
IdrisDependent + linear (QTT)strongparametric + interfacesType-driven development
LeanDependentstrongparametric + typeclassesHygienic macros + elaboration
Coq (Rocq)Dependent (CIC)strongparametric + typeclassesProp / Set / Type universes
AgdaDependent + quantitativestrongparametric + instance argsCubical Agda; reflection
SmalltalkDynamicn/adispatch on receiverdoesNotUnderstand:

Decision rubric

Pick dynamic when iteration speed beats compile-time safety: scripts, glue code, scientific exploration, REPL-driven prototyping. Python, Ruby, Common Lisp, R.

Pick gradual when you have a large dynamic codebase you want to migrate, OR when you want safety where it matters but not everywhere. TypeScript, Sorbet, Python+mypy.

Pick static nominal when you have many engineers, long-lived code, OO domain models. Java, C#, Kotlin, Swift.

Pick HM-style ML when expressivity per syntax matters and you don’t need OO inheritance. OCaml, F#, Haskell, Roc, Gleam.

Pick traits/typeclasses when you want zero-cost abstraction with constraint-based polymorphism. Rust, Haskell, Swift protocols, Scala 3 given/using.

Pick dependent types when correctness must be proved, not tested. Lean, Coq/Rocq, Idris, Agda. Realistic adoption: protocol verification, compilers, security-critical kernels, math formalization. Not realistic: web apps.

Pick refcaps / linearity when data-race freedom or resource correctness must be statically guaranteed. Pony for actor-model concurrency. Idris/Linear Haskell for resource-typed APIs (filehandles that can’t be double-closed).

Edge cases & nuances

  • TypeScript’s structural typing is the famous source of “looks-the-same-but-isn’t” bugs — branded/nominal types via phantom fields are the workaround.
  • Java’s generic erasure vs C#/.NET’s reified generics — same surface syntax, dramatically different runtime model. C# can typeof(T) at runtime; Java can’t.
  • Go’s generics (since 1.18) are deliberately conservative; no HKTs, no variance, weak inference. The design rejects most of the FP type-theoretic tradition.
  • Scala 3 is the Swiss Army of static types — you can write Java-flavored, ML-flavored, dependently-typed-lite, or set-theoretic code. The cost is a learning cliff.
  • Rust’s lifetimes are not types in the value sense — they’re erased at compile time. They’re closer to a separate proof system that runs alongside type checking.
  • Julia’s multiple dispatch is dynamic but the type system is rich (parametric + abstract + concrete + invariant generics) — used for dispatch and the JIT specialization, not for safety.
  • Elixir’s set-theoretic types (1.18+) is the most novel mainstream type system in years — sound gradual + structural + intersection/union/negation.
  • Coq’s rename to Rocq (May 2025) doesn’t affect the type system; same Calculus of Inductive Constructions.
  • HM inference is not decidable in the presence of higher-rank types — Haskell’s RankNTypes requires explicit annotations. OCaml’s first-class polymorphism does too.

Cross-references

For per-language detail on type system depth, see each note’s Intermediate and Advanced sections. The 51 individual notes are linked from _index.

Citations

  • Pierce, Types and Programming Languages (TAPL) — the canonical textbook.
  • Pierce et al., Software Foundations — interactive in Coq/Rocq.
  • Castagna et al., “Set-Theoretic Types for Polymorphic Variants” — the Elixir 1.18 type system theory.
  • Brady, Type-Driven Development with Idris — QTT and dependent types intro.
  • TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/2/
  • Rust Reference (type system): https://doc.rust-lang.org/reference/types.html
  • The 51 per-language notes in _index are the underlying source.