Skip to content

Type Checking — Static, Dynamic and Hindley-Milner

DodaTech Updated 2026-06-21 7 min read

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

What is the difference between Type Checking and type inference?

Type Checking verifies that existing type annotations are consistent with usage. Type inference deduces types without requiring annotations. TypeScript does Type Checking; Haskell does type inference.

Can a dynamically typed language benefit from Type Checking?

Yes. Tools like Mypy for Python and TypeScript for JavaScript add optional static Type Checking to dynamically typed languages, combining flexibility with safety.

What is gradual typing?

Gradual typing allows mixing typed and untyped code within the same program. Parts of the program have static Type Checking; other parts use dynamic typing with runtime checks at the boundary.

How do generics affect Type Checking?

Generics (parametric polymorphism) require the type checker to handle type parameters. The checker must instantiate generic parameters at each use site and verify constraints on type arguments.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro