Type Checking — Static, Dynamic and Hindley-Milner
Type Checking is the Process of verifying that operations in a program receive values of the correct types, ensuring type safety and preventing runtime errors caused by type mismatches in compiled and interpreted languages.
What You'll Learn & Why It Matters
In this tutorial, you will learn the differences between static and dynamic type systems, how type inference works (including Hindley-Milner), and how to implement type checkers for practical programming languages. Type systems catch entire categories of bugs at compile time.
Real-world use: TypeScript catches type errors in millions of JavaScript projects daily. Durga Antivirus Pro uses type-aware analysis to detect type confusion vulnerabilities in compiled code, a class of bugs that accounts for 15% of all critical CVEs.
Prerequisites
You should understand Semantic Analysis from the semantic analysis tutorial. Familiarity with AST structures from the AST tutorial is required. Basic knowledge of Python or Haskell type systems helps.
Static vs Dynamic Type Checking
graph TD
subgraph "Static Typing"
S1[Source Code] --> S2[Type Checker]
S2 -->|Pass| S3[Compile]
S2 -->|Fail| S4[Error: type mismatch]
end
subgraph "Dynamic Typing"
D1[Source Code] --> D2[Execute]
D2 --> D3{Type check at runtime}
D3 -->|Pass| D4[Continue]
D3 -->|Fail| D5[Runtime TypeError]
end
style S4 fill:#f44336,color:#fff
style D5 fill:#f44336,color:#fff
| Property | Static Typing | Dynamic Typing |
|---|---|---|
| When checked | Compile time | Runtime |
| Examples | Java, C language, TypeScript | Python, JavaScript, Ruby |
| Type annotations | Usually required | Optional or absent |
| Performance | Faster (no runtime checks) | Slower (runtime overhead) |
| Error detection | Earlier (before deployment) | Later (production crashes) |
Building a Simple Static Type Checker
class TypeEnvironment:
def __init__(self, parent=None):
self.bindings = {}
self.parent = parent
def lookup(self, name):
if name in self.bindings:
return self.bindings[name]
if self.parent:
return self.parent.lookup(name)
return None
def bind(self, name, type_name):
self.bindings[name] = type_name
class StaticTypeChecker:
def __init__(self):
self.env = TypeEnvironment()
self.errors = []
def check(self, ast):
method = f"check_{type(ast).__name__}"
checker = getattr(self, method, None)
if checker:
return checker(ast)
raise TypeError(f"No checker for {type(ast).__name__}")
def check_NumNode(self, node):
return "int"
def check_FloatNode(self, node):
return "float"
def check_StringNode(self, node):
return "string"
def check_BoolNode(self, node):
return "bool"
def check_VarNode(self, node):
type_name = self.env.lookup(node.name)
if not type_name:
self.errors.append(f"Undeclared variable: {node.name}")
return "error"
return type_name
def check_BinOpNode(self, node):
left_type = self.check(node.left)
right_type = self.check(node.right)
if left_type == "error" or right_type == "error":
return "error"
if node.op in ("+", "-", "*", "/"):
if left_type == "int" and right_type == "int":
return "int"
if left_type == "float" or right_type == "float":
if left_type in ("int", "float") and right_type in ("int", "float"):
return "float"
self.errors.append(f"Type mismatch: {left_type} {node.op} {right_type}")
return "error"
return left_type
def check_AssignNode(self, node):
value_type = self.check(node.value)
existing = self.env.lookup(node.name)
if existing and existing != value_type:
self.errors.append(
f"Type mismatch: cannot assign {value_type} to {node.name}:{existing}"
)
self.env.bind(node.name, value_type)
return value_type
def check_IfNode(self, node):
cond_type = self.check(node.condition)
if cond_type != "bool":
self.errors.append("If condition must be boolean")
self.check(node.then_branch)
if node.else_branch:
self.check(node.else_branch)
checker = StaticTypeChecker()
checker.check(AssignNode("x", NumNode(42)))
checker.check(AssignNode("y", StringNode("hello")))
checker.check(BinOpNode(VarNode("x"), "+", VarNode("y")))
for err in checker.errors:
print(err)
Expected output:
Type mismatch: int + string
Hindley-Milner Type Inference
Hindley-Milner is a type inference algorithm that infers types without explicit annotations. It powers the type systems of Haskell, OCaml, and Rust.
The algorithm has two phases: constraint generation and unification.
class HMTypeInferrer:
def __init__(self):
self.type_vars = 0
self.constraints = []
self.substitutions = {}
def fresh_type_var(self):
self.type_vars += 1
return f"T{self.type_vars}"
def infer(self, expr, env):
if isinstance(expr, NumNode):
return "int"
elif isinstance(expr, BoolNode):
return "bool"
elif isinstance(expr, VarNode):
if expr.name in env:
return env[expr.name]
raise TypeError(f"Unbound variable: {expr.name}")
elif hasattr(expr, 'param') and hasattr(expr, 'body'):
param_type = self.fresh_type_var()
new_env = {**env, expr.param: param_type}
body_type = self.infer(expr.body, new_env)
return ("->", param_type, body_type)
elif hasattr(expr, 'fun') and hasattr(expr, 'arg'):
fun_type = self.infer(expr.fun, env)
arg_type = self.infer(expr.arg, env)
result_type = self.fresh_type_var()
self.constraints.append((fun_type, ("->", arg_type, result_type)))
return result_type
raise TypeError(f"Cannot infer type for {type(expr).__name__}")
def unify(self, t1, t2):
t1 = self.apply(t1)
t2 = self.apply(t2)
if t1 == t2:
return
if isinstance(t1, str) and t1.startswith("T"):
self.substitutions[t1] = t2
elif isinstance(t2, str) and t2.startswith("T"):
self.substitutions[t2] = t1
elif isinstance(t1, tuple) and isinstance(t2, tuple) and t1[0] == "->" and t2[0] == "->":
self.unify(t1[1], t2[1])
self.unify(t1[2], t2[2])
else:
raise TypeError(f"Cannot unify {t1} with {t2}")
def apply(self, t):
if isinstance(t, str):
while t in self.substitutions:
t = self.substitutions[t]
return t
if isinstance(t, tuple):
return tuple(self.apply(x) if isinstance(x, (str, tuple)) else x for x in t)
return t
class LambdaNode:
def __init__(self, param, body):
self.param = param
self.body = body
inferrer = HMTypeInferrer()
expr = LambdaNode("x", BinOpNode(VarNode("x"), "+", NumNode(1)))
result = inferrer.infer(expr, {})
print(f"Inferred type: {inferrer.apply(result)}")
Expected output:
Inferred type: ('->', 'int', 'int')
Common Errors in Type Checking
Error 1: Subtyping Confusion
int is not a subtype of float in all languages. Passing an int where float is expected may require explicit conversion in some type systems.
Error 2: Covariance vs Contravariance
Function types are contravariant in their argument types and covariant in their return types. Getting this wrong produces unsound type systems.
Error 3: Recursive Type Handling
Recursive types (linked lists, trees) require special handling with fixpoint operators or iso-recursive types to avoid infinite type expansion.
Error 4: Overload Resolution Ambiguity
Multiple functions with the same name but different types require careful overload resolution. Ambiguous calls must be rejected with clear error messages.
Error 5: Type Escape in Pattern Matching
Exhaustive pattern matching should be required. Non-exhaustive patterns can cause runtime crashes even in statically typed languages like Haskell and Rust.
Practice Questions
Question 1
What is the difference between static and dynamic Type Checking?
Show answer
Static Type Checking occurs at compile time before program execution. Dynamic Type Checking occurs at runtime when operations are executed. Static checking catches errors earlier but requires more annotations.Question 2
What is type inference?
Show answer
Type inference automatically deduces the types of expressions without requiring explicit type annotations. Hindley-Milner is the most famous type inference algorithm, used in Haskell, OCaml, and Rust.Question 3
What does it mean for a type system to be sound?
Show answer
A sound type system guarantees that well-typed programs never encounter runtime type errors. If a program type-checks, its execution will never perform an invalid operation on a value.Question 4
What is the difference between nominal and structural typing?
Show answer
Nominal typing considers two types equal only if they have the same declared name. Structural typing considers types equal if they have the same structure (fields and methods), regardless of names.Question 5
What is a type class in Haskell?
Show answer
A type class defines a set of operations that a type must support. It enables ad-hoc polymorphism (overloading) without subtyping. For example, the `Eq` type class requires `==` and `!=` operations.Challenge
Implement a type checker for a small language with generics (parametric polymorphism). Support generic functions like identity[T](x: T) -> T and generic structs like List[T]. Ensure the type checker correctly instantiates type parameters at call sites and verifies type arguments.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro