8.1 Static vs Dynamic Type Checking
Type systems answer a practical question: what facts about a program can the language establish before execution, and what facts must be checked while values are already moving at runtime? A static type checker tries to reject invalid programs before they run. A dynamic type checker delays some checks until a value actually reaches an operation such as +, a field access, or a function boundary.
Neither label means “good” or “bad” by itself. The real tradeoff is when information becomes available, who gets blamed for misuse, and how much freedom or predictability the language wants to provide.
| question | static checking | dynamic checking |
|---|---|---|
| when is an error discovered? | before execution | during execution |
| what facts help optimization? | types are known earlier | more facts appear only at runtime |
| what does the compiler own? | explicit type facts in the frontend | fewer guarantees before execution |
| failure style | compile-time rejection | runtime trap or exception |
Early Rejection Changes the Whole Pipeline
Suppose a language sees:
let x: int = "hi"
print(x + 2)In a static system, the program is already in trouble before it reaches code generation. The checker knows that x is declared as int, sees that "hi" is a string, and can reject the assignment immediately. That early rejection matters because later phases do not need to guess whether x + 2 is meaningful. Constant folding, overload resolution, and IR lowering all benefit from a stronger contract.
In a dynamic system, the assignment may succeed because the variable can hold a runtime-tagged value. The error appears only when execution reaches x + 2 and the runtime discovers that string + int is not permitted in that language. The program may even run for a while before it fails.
Blame Is Part of the Design
Type checking is not only about catching errors. It is also about assigning blame to the most useful location.
Consider takesInt("x"). A static checker usually blames the call site, because it already knows the parameter type and the argument type. A dynamic checker may blame the function entry, where a runtime contract verifies that the incoming value is an integer. Both are defensible, but they create different debugging experiences.
The same issue appears in conditionals and data access:
| program shape | static system often blames | dynamic system often blames |
|---|---|---|
| wrong argument type | call site | callee boundary |
| inconsistent branch result | branch join point | later consumer of the result |
| missing field | member access | runtime property lookup |
This difference explains why static systems often produce earlier and narrower diagnostics. The compiler owns more facts earlier, so it can stop the mistake closer to its source.
Reachability and Flexibility
Dynamic checking has one practical advantage: code that is never executed may never be checked at all. That can be useful in reflective systems, scripting environments, plugin architectures, or languages that want great runtime flexibility. But it also means some errors survive longer and are discovered only under particular inputs.
Static checking, by contrast, reasons about potential execution paths without waiting for them to happen. That usually improves refactoring safety, editor tooling, and optimization, but it may reject programs that a highly dynamic language would permit.
The best comparison is therefore not “which one is smarter?” but “which stage should own the promise?” A language that wants strong compile-time guarantees pushes more responsibility into the frontend. A language that wants runtime openness keeps more checks alive while the program runs.
Worked Boundary Case
Imagine:
if debug {
takesInt("x")
}If debug is false at runtime, a dynamic system may never complain. A static system still rejects the call because the branch is part of the program text and its misuse is visible before execution. This is not a contradiction. It is exactly the policy difference between reasoning about the whole program structure and reasoning about the path that actually ran.