Programming Language Typing Reference

Static vs dynamic, strong vs weak typing for 40+ languages.

Reference of programming language type systems with static, dynamic or gradual checking, strong or weak coercion, nominal versus structural and duck typing, plus type-inference notes and a live filter. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

What is the difference between static and dynamic typing?

Static typing checks types at compile time, catching mismatches before the program runs (Java, Rust, Go). Dynamic typing checks types at run time, so type errors surface during execution (Python, JavaScript, Ruby). Static catches errors earlier; dynamic is more flexible.

Two independent axes of type systems

A language’s type system is usually described along two separate axes: when types are checked (static, dynamic or gradual) and how strictly mismatches are handled (strong or weak). These are independent — Python is dynamic but strong, C is static but weak. This reference compares both axes across major languages, along with nominal-versus-structural and type-inference notes.

The four quadrants

              Strong typing                 Weak typing
Static     Rust, Java, Haskell, Go       C, (C++ casts)
Dynamic    Python, Ruby, Clojure         JavaScript, PHP, Perl, Lua
Gradual    TypeScript, Python+hints      —

Static + strong (Rust, Java, Haskell, Go) — types are checked at compile time and mismatches are errors, not silent coercions. This combination catches the largest class of bugs before the program runs, at the cost of more verbose annotations (though inference reduces this in Haskell and Rust).

Static + weak (C, C++ with implicit casts) — types are checked at compile time, but the language allows implicit coercions between numeric types, pointers, and arrays. The compiler knows the types but will silently convert int to float or allow pointer arithmetic without complaint. C’s “undefined behaviour” territory arises largely from this combination.

Dynamic + strong (Python, Ruby, Clojure) — types are checked at run time, not compile time, but the language refuses to mix incompatible types without explicit conversion. Python raises a TypeError if you add a string to an integer. This catches mismatches later than a static language, but does not quietly produce wrong results.

Dynamic + weak (JavaScript, PHP, Perl, Lua) — types are checked at run time, and the language performs implicit coercions aggressively. JavaScript’s 1 + '2' = '12' is the canonical example: the addition coerces the integer to a string rather than raising an error. These languages prioritise flexibility over predictability.

Gradual (TypeScript, Python with type hints, Dart) — a pragmatic middle ground. Static analysis is opt-in: annotated code is checked at compile time; unannotated or any-typed code bypasses checking. This lets you add types to an existing codebase incrementally.

Nominal versus structural typing

Nominal typing matches types by their declared identity. In Java, a class Dog is only compatible with another Dog or something that explicitly extends Dog or implements Animal. Two classes with identical fields are not compatible unless one inherits from the other.

Structural typing matches by shape. TypeScript and Go interfaces work this way: if a value has the required fields or methods, it satisfies the type regardless of what it is called. This eliminates boilerplate “I’m implementing this interface” declarations but requires care — two structurally compatible types may be semantically incompatible even though the compiler cannot tell.

Duck typing is the dynamic runtime version of structural typing: “if it has a .speak() method, call it as though it is an Animal, and if it fails, handle the error at runtime.” Python, Ruby, and most dynamic languages use this as the default approach to polymorphism.

Type inference

Several statically-typed languages eliminate much of the annotation burden through inference — the compiler or interpreter deduces the type of an expression from how it is used.

  • Haskell has the most powerful inference (Hindley-Milner): full type information is usually inferable without a single annotation.
  • Rust infers types within function bodies; function signatures still need explicit annotations for the public API.
  • Kotlin and Swift infer from the right-hand side of assignments, removing the need for explicit type declarations in most variable definitions.
  • Go infers inside function bodies when using := short variable declaration, but struct fields and function signatures require explicit types.

Practical guidance

  • “Strong” and “static” are not synonyms. Keep the two axes separate when comparing languages — conflating them is the most common type-system confusion.
  • Structural typing reduces boilerplate (no implements FooInterface declarations) but can produce accidental compatibility bugs where two distinct types pass the same check.
  • Gradual type systems are valuable when migrating a dynamic codebase. Start by annotating the critical paths and public APIs; the any escape hatch exists for parts of the codebase you have not reached yet.
  • Type inference matters most in static languages — it is what makes Rust and Kotlin ergonomic to write despite being fully statically typed.