Every program works with values of different types: numbers, strings, lists, users, orders. The question is when those types get checked.
- **Static typing**: The types are checked before the program runs, by a compiler or a type checker. Java, C#, Rust, Go, Haskell and TypeScript work this way
- **Dynamic typing**: The types are checked while the program runs, value by value. JavaScript, Python and Ruby work this way by default
With static typing, calling `user.nmae` or passing a string where a number is expected fails at build time. With dynamic typing, you find out when that line actually runs (sometimes in production, months later).
**Type safety**:
Type safety is the guarantee that a program can't perform an operation on a value of the wrong type. Static typing is one way to get it, but the two terms aren't the same thing. C is statically typed and still lets you reinterpret memory as any type you like. Python is dynamically typed and still refuses to add a string to a number. In practice, the strongest guarantees come from languages that are both statically typed and type safe.
**What static types give you**:
- **Earlier feedback**: Many bugs show up in your editor, while you type
- **Safer refactoring**: Rename a field, and the compiler lists every place that needs to change
- **Documentation that can't go stale**: Function signatures describe what goes in and what comes out
- **Better tooling**: Autocompletion, navigation and inline errors all rely on type information
- **Design tools**: Option types, newtypes and sum types let you encode domain rules directly (see Make Illegal States Unrepresentable)
**Gradual typing**:
Jeremy Siek and Walid Taha coined the term "gradual typing" in 2006. The idea is to mix static and dynamic typing in the same program, and to add types progressively. TypeScript does this for JavaScript, and Python's type hints (PEP 484, checked by tools like mypy) do it for Python. You can start with an untyped codebase and add types where they pay off most.
**The limit: types vanish at runtime**:
TypeScript's types are erased when the code is compiled to JavaScript. The article on TypeScript's non-goals explains this design choice: TypeScript aims to give developers as much certainty as possible at development time, and does not keep type information at runtime. That's why runtime checks still matter for data coming from back-ends or external clients. A type annotation on an API response is a promise, not a check.
The fix is to parse untrusted data at the boundary (see Parse, Don't Validate), for example with a schema library like Zod, and let the static types take over from there.
**The tradeoffs**:
Static types add some upfront work and can feel restrictive for quick experiments. Type systems also can't express every rule. Still, the bigger and longer-lived a codebase gets, the more those early, automatic checks are worth.