Add a number to a number and you get a number. Add a number to a function, or call a string as if it were a procedure, and you have written something meaningless — an operation that has no sensible answer. A type system is the set of rules a language uses to spot such nonsense and rule it out before it can run.
The core idea is small and powerful: every phrase of a program — a literal, a variable, an
expression, a function — is assigned a type, a label describing what kind of value
it stands for (
This page builds on the
JavaScript is famously permissive. The expression "1" + 1 does not raise an
error — it silently decides the + means "join text" and hands you the string
"11". Later arithmetic then produces garbage, and nothing ever complained. A stricter
type system would have stopped you at the +. Run this, then read what happened:
The surprise is line one: "5" + 1 is "51", not 6. The type
annotation n: number on doubleIt is the fix — TypeScript reads it and
rejects doubleIt("21") before the program ever runs, forcing you to convert
with Number(...) and make your intent explicit. The type is a promise the checker
enforces.
The first great division is about timing.
TypeError: 'int' object is not callable. Python, Ruby, JavaScript and Lisp are dynamic.
The payoff: flexibility and terse code; the cost: the error surfaces only when execution actually
reaches that line.
A slogan: a static type checker asks "could this ever go wrong?" and a dynamic one asks "did this just go wrong?". Static typing trades a little up-front ceremony for guarantees that hold across every possible run.
The second division is a different question — not when types are checked, but how firmly the rules are enforced once you hit a mismatch.
"1" + 1 raises TypeError — it will not guess.
"1" + 1 === "11" is a weak,
implicit coercion. C is weaker still: a cast or a union lets you read the
very same bytes as an int one moment and a float the next — no conversion,
just a reinterpretation the compiler permits.
"Strong" and "weak" are really a spectrum, not a switch — languages sit at points along it — but the essential test is: when the types don't match, does the language stop you, or does it shrug and reinterpret?
The single most common confusion in this whole topic: strong/weak is NOT the same axis as static/dynamic. They are two independent dials, giving a 2×2 grid — and every box is occupied by a real, famous language:
"1" + 1
is refused, not coerced."5" + 1 into "51".So "Python is dynamic" tells you nothing about whether it coerces — and in fact it is fairly strong. Keep the two questions separate: when is it checked (static/dynamic) vs how strictly (strong/weak).
The types a language knows about aren't a flat list — they are ordered by a
subtype relation. If every value of type unknown / Object / any) that
every type is a subtype of, and a bottom type (never) that is
a subtype of everything — the type of an expression that never produces a value at all.
Together this forms a lattice, drawn below.
Reading it top-down: unknown sits above the concrete types number,
string and boolean; each of those sits above never at the
base. Widening (moving up the lattice, e.g. treating a number as
unknown) is always safe. Narrowing (moving down) is where checks are needed —
and where an unsound system can let a lie slip through.
Why trust a type checker at all? Because of a property called soundness. A type system is sound if it never lies: whenever it says a program is well-typed, that program really will not commit a type error at run time. Robin Milner captured it in 1978 in one of computer science's most quoted lines:
Soundness is what turns a type annotation from a comment into a guarantee. In a sound, statically-typed language an entire family of crashes — "undefined is not a function", "null pointer", "added a string to a list" — simply cannot occur in accepted code. That is the safety you are buying.
Because most languages are deliberately unsound in places, trading a guarantee for
power or convenience. C's casts and pointer arithmetic are escape hatches the compiler cannot check
— so C is type-checked but not type-safe. TypeScript's any and its type
assertions (x as Foo) punch sound holes on purpose, to model messy real-world
JavaScript. Even Java's array covariance is a known unsoundness patched by a runtime check.
A perfectly sound system also rejects some programs that would actually have been fine — no checker
can be both perfectly sound and perfectly permissive, so language designers pick where to sit.
Put the two axes together and every language lands somewhere. A rough field guide:
any, assertions) for JS compatibility."1" + 1 rather than coercing.