DEV Community

Cover image for I grepped for @ts-ignore and found zero. Then I grepped for the cast nobody bans

I grepped for @ts-ignore and found zero. Then I grepped for the cast nobody bans

chet100 on November 01, 2026

Every TypeScript team has a rule about @ts-ignore. Ours does too, and it works: zero occurrences across 610 source files. Then I grepped for somet...
Collapse
 
finley_works profile image
Finley Sullivan •

as unknown as is the sneakiest one.

Collapse
 
logan_works profile image
Logan Jenkins •

grepping for casts is such a simple trick — definitely stealing this

Collapse
 
stackmasteraliza profile image
Aliza Ali •

Love that the allowlist entry has to be a reviewable sentence. One gap in the grep itself: it won't catch the cousins, like as any as Foo or a double cast split across lines by Prettier. If you want an AST-aware version of the same budget, typescript-eslint's no-unsafe-type-assertion rule (typed linting) flags assertions that widen past what the compiler can prove, including those variants, and you can still keep your per-file allowlist through eslint-disable comments with a required description.

Collapse
 
lin7c profile image
xcl •

The clustering is what makes the budget approach convincing here: 14 of the 117 in admin.ts and 11 in stories/worker.ts means this is not random sloppiness, it is a couple of boundary types that never got parsers. My question is about the ratchet mechanics: is your budget a single repo-wide count, or per file? A single count could let a new cast in a low-risk file hide behind a cleanup in admin.ts, whereas per-file budgets would force each boundary to improve on its own, but they would also be noisier to maintain.

Collapse
 
chet103cc profile image
chet100 •

I tried the per-file version while reading this, mostly to check whether the maintenance cost you're worried about is real. Across the two app trees, the 117 casts live in 38 files — 15 of them have exactly one, and the two files you spotted hold 25 — so the baseline is a ~40-line JSON, which is small enough that I stopped worrying about the noise. The churn case turns out to be a rename: one removed key plus one added key in the diff, which reads as honest rather than noisy. And the laundering you described gets caught straight away — cleaning three out of admin.ts while adding one in a one-cast file moves the repo total down by two, so a single counter stays quiet, while the per-file diff flags both lines. So I'm with you on per-file as the mechanism. The only thing I'd still push back on is leaving it there permanently: those two files are effectively the whole job, and once they get parsers, a single number is enough to police what's left.

Collapse
 
amorizz profile image
Amorizz •

The no-restricted-syntax selector in the gate section is wider than the grep. TSAsExpression > TSUnknownKeyword matches any x as unknown, including a single cast like JSON.parse(s) as unknown, which is the honest version. So the lint count and the grep count will drift apart. To hit only the double cast, select the outer node: TSAsExpression[expression.type='TSAsExpression'][expression.typeAnnotation.type='TSUnknownKeyword']. esquery handles the dotted attribute path. Also, grep counts lines, so two casts on one line count once. I'd run both against the same tree once and check the numbers agree before trusting either as the ratchet.

Collapse
 
maxbuildsdev profile image
Max Kaya •

The save-the-grep point is underrated and worth its own section. When I've run similar audits, the grep is what survives org change: the ESLint config gets consolidated into a shared preset, the parser plugin version drifts, and the selector that matched TSAsExpression silently stops matching — nothing errors, the rule just stops firing, and six months later nobody remembers why the count is nonzero again. A grep in CI with a hardcoded threshold fails loudly instead. That said, the grep misses as any as T spelled through a helper like function cast<T>(x: unknown): T { return x as T } wrapping a double cast — did your 117 count include cast helpers, or would you treat a single-purpose cast function as a legitimate pattern 4 (a parser you didn't write yet) and only flag call sites with suspicious argument shapes?

Collapse
 
parker_58 profile image
Taylor Parker •

the as unknown as part hit a little too close to home

Collapse
 
sherotree profile image
Sherotree •

117 as unknown as with zero @ts-ignore is the Grecian-formula ban problem in the wild. The cluster on admin routes and workers is the useful signal — those edges need parsers, not another lint rule people learn to dodge.

Collapse
 
kielltampubolon profile image
Kiell Tampubolon •

One gap the budget can't see is the version with no cast at all. const row: JobRow = JSON.parse(body) compiles cleanly because JSON.parse returns any, and so does await res.json(). Once a team ratchets as unknown as down, I'd expect some of those 117 to come back as plain annotations, which is the same lie with nothing left to grep.

The fix I'd reach for is making the boundary return unknown instead of any (ts-reset does this for JSON.parse and .json()), so the compiler forces either a parser or a visible cast. Then the budget counts every escape hatch, not just the one people can see.

Collapse
 
paullovelace profile image
Paul Lovelace •

The budget-not-a-ban rule is the bit I'll actually copy — a hard zero on day one just buys you a 117-line allowlist nobody reads. And keeping the grep even with the ESLint selector is the honest part; I've watched a parser upgrade silently unmatch a selector before. Do the 15 single-cast files bother you more than the two big clusters, or are those the ones that are easiest to just leave alone?

Collapse
 
alexshev profile image
Alex Shev •

This is a good reminder that code-quality rules should target the escape hatch, not the fashionable spelling of it. The next step is to classify casts by boundary: parsing untrusted input, crossing a library seam, or silencing an internal mismatch. Those cases deserve very different review and test pressure.

Collapse
 
sgaggjhkjh profile image
Arjun Sharma •

The budget rather than a ban is the part I would adopt first, because a hard zero would just create an unread allowlist. How do you handle a cast that is genuinely needed at a boundary but has no clean parser yet, does it get a dated allowlist entry or a separate exception?

Collapse
 
sherotree profile image
Sherotree •

The clustering table is the part I keep — once you see 14 casts in admin.ts, the fix stops being "ban the cast" and starts being "one request parser for that route." Same pattern as the 42 identical no-img-element disables: one architectural hole, not forty-two decisions.

Collapse
 
ascendra_ventures_92c21f2 profile image
Ascendra Ventures •

Zero ts-ignore and 117 "as unknown as" is a great example of a rule being followed while its purpose is lost. The cast does the same job as the ignore, it just does not show up in the lint report.

The clusters are the useful finding. 14 in admin.ts and 11 in the worker file suggest those are boundaries where data comes in without a parser, so the casts work like a map of where validation is missing.

Your note that agents reach for this cast matches what we see. A model under pressure to make the types pass will choose the shortest path, and a CI budget with an allowlist is a fair way to make that visible without blocking every change.

At Ascendra Ventures we include a check like this in handover for client codebases, because it shows where the next bug is likely to come from. Did you replace the admin.ts casts with schemas, or keep some on the allowlist?

Collapse
 
mudassirworks profile image
Mudassir Khan •

the 117 vs 0 ratio is the real finding. we saw the same thing after banning any: double casts grew fastest on agent written branches because it is the smallest edit that makes tsc go quiet.

the budget ratchet beats a hard zero, agreed. one thing i would add is counting per directory, since routes and workers will always hold most of them and a single global number hides a new cluster showing up somewhere it should not.

are you tracking the budget per package or one number for the whole repo?

Collapse
 
kartik-nvjk profile image
Kartik N V J K •

Your CI message gives whoever hits the budget two options, narrow the type or add an allow line with a reason. I'd expect a coding agent to pick the second one, because a plausible sentence after as-unknown-as-allow is cheaper than writing the parser. Counting new allow markers in the diff would at least send a PR that adds them to a human.

Collapse
 
mp3sss31 profile image
mp3sss •

This is exactly the lesson style-guide bans keep relearning: you don't remove the escape hatch, you just rename it - 'as unknown as' is @ts-ignore with extra steps, and the 117-vs-0 ratio shows enforcement only works when the linter, not the reviewer, owns the rule. I'd add that the fix isn't just banning the cast syntax: it's requiring a type-guard or a runtime schema next to the cast so the coercion is validated where the data actually enters. The eslint-disable clustering is a nice tell too - 42 of 53 coming from one rule is usually an architecture decision that should be configured once, not suppressed per-file.

Collapse
 
murphy_90 profile image
Ira Murphy •

The grep trick is underrated because it turns a vague code review argument into a concrete number. We started tracking cast counts per file and it changed the conversation.