skip to content

Static Type Checking

Proving type safety before runtime: inference, control-flow narrowing, gradual typing and the soundness-versus-completeness trade-off. Asked to see which bug classes a checker never catches.

on this pageshow

questions

4

What does a static type checker prove before a program runs, and which bugs does it never catch?

level: juniorimportance: must knowfreq 58%

answer

  1. It reads the code, never runs it
  2. Every path, not just the tested one
  3. Operations receive types they accept
  4. Meaning and domain rules stay unchecked
  5. External data is an assumption until validated

basics

~20 s

A static type checker proves, without running the program, that every operation receives a value whose declared or inferred type it accepts. It rules out type-mismatch errors on all code paths, but says nothing about whether the logic is right.

solid answer

~50 s

A static type checker reads the source, assigns a type to every expression, and rejects the program if any value flows into an operation that does not accept it. Because it reasons over the text rather than one run, its verdict covers every reachable path, including paths no test exercises: calling something that is not callable, arguments in the wrong order when the types differ, reading a member that does not exist, missing a case in a closed set of alternatives, and using a possibly-absent value where the type system models absence. What it does not check is meaning. A payroll routine that prorates by calendar days instead of working days type-checks perfectly, because both are counts. It also knows nothing about values that only exist at run time — parsed input, configuration, replies from other services — until code validates them at the boundary.

code

pseudocode · 9 lines
pseudocode
function payslipTotal(hours: Number, rate: Money) -> Money:
    return rate * hours

# rejected before running: argument types do not match the signature
payslipTotal(rate, hours)

# accepted: both amounts are Money, but the rollback reverses the wrong one
function rollbackPosting(posting):
    credit(posting.employee, posting.gross)   # posting wrote posting.net

go deeper

for a junior

Be ready to state the guarantee in one sentence — every operation gets a value of a type it accepts — and to name two bug classes it misses, such as wrong arithmetic and unvalidated external input.

for a middle

Explain the mechanics: inference, control-flow narrowing, and erasure before execution. Be able to say why a fully annotated program still needs boundary validation and what the checker does at a changed signature.

for a senior

Show the production judgment: which failures in your incident history the checker would have caught, and where you deliberately encoded a domain distinction as a distinct type so a logic bug became a compile error.

for a principal

Own the framing that a checker's power scales with how much meaning the types carry, and decide where the team invests annotation effort versus tests, boundary validation, and runtime assertions.

## What "static" means here An analysis is *static* when it reasons about the program text instead of a particular execution. A type checker builds a model of every declaration and expression, assigns each one a type, and then asks one mechanical question of every operation in the program: does the type of the value flowing in belong to the set of types this operation accepts? If the answer is "no" anywhere, the program is rejected — before a line executes, before a test runs, and on **every reachable path at once**. That last clause is why type checking sits in its own box among static analyses. A test reports on the one path it drove; a checker's verdict is universal over the code it can see. ## What the proof actually covers The guarantee is narrow and precise. In broad strokes a checker establishes that: - every operation is applied to a value of a type it accepts (you cannot call a non-callable value, add a value to something the addition is not defined on, or read a member the type does not declare); - calls match the declared shape — arity, parameter types, and return type — so a caller cannot silently disagree with a callee; - where the type system models a closed set of alternatives, every alternative is handled, so adding a new one turns into a compile error at each place that must change; - where the type system models absence, a possibly-absent value cannot be used as if it were present without handling that case. The practical dividend most teams feel first is refactor safety. Change a signature and every stale call site becomes an error rather than a run-time surprise, which is what makes wide mechanical refactors affordable at all. ## What it never covers A type checker is blind to meaning. It does not know your domain rules, your ordering constraints, your rounding policy, or your money. Two quantities with the same type are interchangeable as far as it is concerned, and most real defects live exactly there. It also does not, in general, verify resource lifetimes, concurrency correctness, performance, or the shape of data that only comes into existence at run time. Nor does it replace security analysis, which is a different class of analyser with a different question. There is a technique for pulling some logic bugs into the checker's reach: give distinct domain quantities distinct types instead of sharing one primitive type. Wrapping a gross amount and a net amount in two different types turns "I mixed them up" from a silent arithmetic bug into a rejected program. This is the single most useful move a candidate can name here, because it shows they understand that the checker's power is entirely a function of how much information the types carry. ## The run-time boundary Annotations on data that crosses in from outside — request payloads, configuration, stored records, replies from another service — are **assumptions**, not guards. In most checked languages the types are erased before execution, so nothing enforces the declaration at run time. What makes the assumption true is explicit validation at the boundary; the checker's job is to propagate the validated fact inward so no downstream code has to re-check it. Candidates who miss this ship a program that is "fully typed" and still crashes on the first malformed record. ## Inference and narrowing, briefly Two mechanisms keep strict checking bearable. *Inference* lets the checker derive a type from an initialiser or a return expression, so annotations are needed only where the intent is not evident. *Control-flow narrowing* lets it refine a type along a branch after a test — once a branch has established a value is present or is one particular alternative, the checker uses the narrower type inside that branch. Both exist so strictness does not cost annotation noise on every line, and their limits are why some code still needs an explicit annotation. ## A worked pair from one codebase A 4-person team maintains a payroll engine. In one quarter two changes show both sides of the guarantee. *Caught:* the batch-run identifier changed from a plain numeric id to a composite key. The first compile listed 31 stale call sites, 6 of them inside a partial-failure rollback path that no test exercised. None of those 6 would have been found by the suite; the checker found them for free, on a path nobody was running. *Missed:* the compensating routine in that same rollback reversed the **gross** figure where the original posting had written the **net** figure. Both are the same numeric type, so nothing was flagged, the build was green, and 312 payslips carried a 21.47 discrepancy. The fix was not more tests alone — it was giving gross and net distinct types so that mixing them is a compile error from then on. ## How to answer this in an interview State the guarantee precisely, then state the limits honestly, then say what you do about the limits: validate at boundaries, model domain quantities as distinct types, and keep tests for behaviour the types cannot express. The failure mode interviewers listen for is "it compiles, so it works", or the claim that a type checker removes the need for tests.

  • If types are erased before execution, why bother annotating data that arrives from an external service?
    The annotation is a claim about that data, and the checker propagates it to every downstream use, so nothing inside re-checks it. That is only sound if one place actually validates the payload at the boundary and converts it into the declared type. Skipping the validation gives you a fully annotated program that still fails on the first malformed record, with the failure surfacing deep inside code the checker declared safe.
  • What is control-flow narrowing, and why does it matter for a strict checker?
    Narrowing is the checker refining a value's type along a branch because the branch condition established something about it — that it is present, or that it is one particular alternative of a closed set. Inside that branch the value is used at the narrower type with no annotation and no cast. Without narrowing, strict checking would force casts or assertions everywhere, and those escape hatches are exactly what erodes the guarantee.
  • Does a type checker replace unit tests?
    No. They prove different things. The checker proves type consistency over all reachable code, including paths no test drives, and costs nothing per path. Tests prove behaviour: the rounding rule, the ordering, the domain policy — all things that type-check whether they are right or wrong. The useful framing is that each one covers what the other cannot, and pushing domain distinctions into the types shifts a slice of the second category into the first.

A type checker is like a warehouse that refuses any pallet which will not fit the racking: it guarantees everything on the shelves fits the shelves, not that anyone ordered the right goods.

saying these in an interview costs you the question

  • Says a program that compiles is therefore correct
  • Claims types remove the need for tests
  • Thinks annotations are enforced at run time
  • Believes the checker validates external input automatically
  • Cannot name a bug class the checker misses
  • Confuses type checking with security scanning

context

open as a page

What is gradual typing, and what guarantee is lost where typed code meets untyped code?

level: middleimportance: should knowfreq 44%

basics

~20 s

Gradual typing lets annotated and unannotated code coexist in one program: annotated regions are checked, unannotated regions are trusted. At the boundary the checker's proof stops, so a checked region can still receive a value that violates its declared type.

open as a page

As a lead, how do you decide whether adding a static type checker to an untyped codebase pays off?

level: principalimportance: should knowfreq 33%

basics

~20 s

Decide from your own defect record and code profile, not from taste: classify past escaped defects by whether types would have caught them, weigh annotation cost against code lifetime, churn and interface fan-out, and define which regions must be covered for the benefit to be real.

open as a page

In a static type checker, what is the difference between soundness and completeness?

level: seniorimportance: nice to knowfreq 21%

basics

~20 s

A sound checker rejects every program that would commit a type error at run time, so it has no false negatives. A complete checker accepts every program that would not, so it has no false positives. Real checkers give up some of one.

open as a page