Skip to content
Blog

Three classes of bug that pass typecheck, ESLint and the build

We reviewed 74 components of our own design system and found 188 issues. They reduced to three causes, and every one of them was invisible to every tool in the chain.

Design systems9 min read

We built a cross-platform design system — 74 components, three targets, 430 tests, and about 2,450 lines of custom verification. Then we reviewed all of it, by reading, and found 188 issues.

The count isn’t the interesting part. What’s interesting is that they reduced to three root causes, and that all three pass the entire toolchain. TypeScript is satisfied. ESLint is quiet. The build succeeds. Every automated gate we had said the code was fine.

Understanding why each one is silent is what turns a review finding into a rule that catches the next one.

One: a name with no bearer

accessibilityLabel on a node that has no role is announced by neither platform.

On the web, aria-label on a bare div does nothing. This isn’t a browser quirk — the ARIA specification says a generic role does not accept an author-supplied name. The attribute is present, spelled correctly, and ignored.

On native, accessibilityLabel without accessible is never read at all. The label exists in the tree; nothing looks at it.

Nine components in our system had this. The label was written, someone had thought about accessibility, and a screen reader announced nothing.

Why every tool is silent: the attribute is valid. It’s the right name, the right type, in the right place. There is no static rule that says this attribute is meaningless unless a sibling attribute is also present, because in general that isn’t true — it’s true only for this pairing, on this kind of node.

What made it findable: the correct pattern already existed in the same package. Icon always pairs its label with role="img". Once you see the working case, the nine broken ones become a search rather than an inspection.

What it became: a verification rule. A label without a role now fails the build.

Two: an inherited prop that never reaches the element

This one is our favourite, because the reason it’s silent is so precise.

A component takes Omit<BoxProps, 'as' | 'ref'>. That leaves id, a11yRole, accessibilityLabel and pointerEvents in the public contract — the types say a caller may pass them, and the caller does.

But a branch that doesn’t explicitly destructure them passes them along in ...box, into a style helper that returns only $-prefixed props. The props are consumed and dropped. They never reach an element.

Why every tool is silent: an omitted prop is undefined, and undefined is a valid value for an optional prop. There is no type error, because nothing was typed wrongly. The contract promised the prop would be accepted, not that it would be used.

That gap — between accepted and used — is not something a type system models. It’s a runtime property of the implementation that the signature cannot express.

How bad it gets: we had four sibling components with four different subsets wired. One passed all four through. Two passed two each, and not the same two. One passed none. Same base type, same intent, four different behaviours — and every one of them typechecked.

What made it findable: comparing siblings. Any one of them read alone looks correct. Read side by side, the inconsistency is obvious in seconds.

Three: a comment describing an earlier version of the system

A note names a limitation. Later the limitation is removed. The note stays.

This is the most insidious of the three, and it’s the one we’d warn people about hardest, because the failure mode isn’t wrong behaviour — it’s wrong belief. A developer reads that something isn’t supported, believes it, and works around a restriction that stopped existing six months ago. The code they write is correct and unnecessary. Nobody ever finds out.

There’s a worse variant. A JSDoc block shifted by one prop during an edit, so one prop carries two descriptions and its neighbour carries none. That’s already bad in the editor. In our case it was worse, because the docs are generated from source into the catalog — so the wrong description became the public documentation of that prop.

Why every tool is silent: it’s a comment. There is no tool anywhere in a JavaScript toolchain that checks whether prose is true. Linters check that JSDoc is well-formed, not that it’s accurate.

What makes this class hard: the other two have a shape you can look for. This one requires knowing what the system currently does and comparing it against what the text claims — which is a reading task, not a checking task. Of the three, this is the one that stays a human problem.

What the three have in common

Every automated gate exists to answer a specific question. Typecheck asks whether the values fit. Lint asks whether the syntax follows the rules. The build asks whether it compiles.

None of them asks: does this attribute do anything? Does this prop reach an element? Is this sentence still true?

Those are questions about meaning, and meaning is exactly what the tools don’t hold.

What we actually changed

Two of the three became verification rules — a label without a role fails, and props declared in a contract must be traceable to an element. The third didn’t, because there’s nothing to check.

The bigger change was to the review itself. Finding 188 issues and fixing 188 issues would have been a waste; finding three causes and fixing those meant the next hundred were prevented rather than found.

And we should have done it at 30 components, not 74. All three causes were already present at 30. They were cheaper to fix then, and every component built afterwards would have inherited the corrected pattern instead of the broken one.

That’s the actual lesson. Not the three causes — yours will be different. The lesson is that a review which stops at the findings has done half the work, and the half it skipped is the half that compounds.

This review was run against our own cross-platform design system, covering Next.js, React Native and a Vite SPA. Read the case study →

Related
The whole blog →

A component library nobody trusts?

We review what your tools cannot see, and turn the findings into rules the build enforces. We reply within one business day.

Get in touch

Systems that can’t stop — from architecture to production.

© 2026 Micro Tech, Sarajevo