The Bug Class Lint Can't See: Why Type Checking Gets Its Own Pipeline Job

Part three of the pipeline series — last time was lint, the fast, per-line check that runs first. This time: type checking, which looks like it should be the same job, and isn't.
Same pipeline stage, different question entirely
Lint asks, roughly, "is this line of code well-formed and free of known bad patterns?" A type checker asks something much bigger: "given every function's declared inputs and outputs, is every call site in this entire program actually consistent?" That's not a per-line question — it's a whole-program question, and it needs a completely different kind of tool to answer it than a rule-based linter does.
I felt this difference directly moving a project from plain JavaScript to TypeScript or when type hints started being used more often in python . Lint had been clean the whole time — no unused variables, no obviously bad patterns. But a function that was supposed to always return a User object was, in one edge case, silently returning undefined. Every call site that used the result immediately assumed it existed. Lint had no way to know that, because lint doesn't track what a function returns across the file boundary into every place that calls it. TypeScript's compiler caught it instantly, because tracing that exact kind of consistency across the whole program is the entire point of a type system.
Here's that bug in miniature. Run lint, then the type checker, then apply the fix and run the type checker again:
Run lint first, then the type checker, then apply the fix.
Why it doesn't just get folded into lint
ESLint and TypeScript's compiler solve genuinely different problems under the hood — one is a rule engine matching patterns in a syntax tree, the other builds and checks a model of every type flowing through your program. Some ecosystems technically let you run both through one wrapper, but they still do separate work and usually still get separate pipeline steps so a failure tells you immediately which category of problem you're looking at: a bad pattern, or an actual type mismatch.
Python's version of this is mypy (or pyright) checking type hints that, notably, don't even affect anything at runtime by themselves — they're purely a compile-time-style check layered onto a language that was never statically typed to begin with. That's a useful thing to know before assuming "my language doesn't have a compiler, so this job doesn't apply to me." If your codebase has any type annotations at all — Python type hints, JSDoc @param types — there's very likely a checker for them, and it's very likely not running yet unless someone deliberately wired it into CI.
What it actually buys you in a pipeline
A type checker is exhaustive but shallow — it checks every code path for internal consistency, but it has no idea what your code is actually supposed to do, only whether the types line up. That's a different, complementary kind of coverage from tests, which are deep but narrow — they verify real behavior, but only for the specific inputs someone bothered to write a test for. Running both isn't redundant. Type checking catches the "this literally cannot be correct" class of bug across 100% of your code, instantly, without anyone writing a single assertion. Tests catch "this is correct in principle but wrong in this specific case." You want both jobs, and you want to know which one just failed.
Next: the test job itself — what "all tests passed" as a merge gate actually guarantees, and, just as importantly, what it doesn't.
Enjoyed this one? Get the next post by email — no spam, unsubscribe anytime.
Subscribe →