Skip to content

Three kinds of design system bug that TypeScript and ESLint miss

Reviewing 75 components turned up 188 issues from three causes: labels on nodes with no role, props accepted and then dropped, and outdated comments.

4 min read5 sections

I built a cross-platform design system. At the time of the review it had 75 components, three targets, 430 tests and about 2,450 lines of custom verification. I reviewed all of it by reading the code and found 188 issues.

The 188 came down to three root causes, and all three pass the entire toolchain: TypeScript, ESLint and the build. Every automated gate I had said the code was fine.

For each cause I worked out why no tool reports it, because that is what turns a review finding into a rule that catches the next case.

01

A label on a node with no role

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

On the web, aria-label on a bare div does nothing. The ARIA specification defines it that way: a generic role does not accept an author-supplied name. The attribute is there, spelled correctly, and ignored.

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

Nine components in my system had this problem. Their labels had been written with accessibility in mind, and a screen reader announced nothing.

No tool flags it because the attribute is valid. It has the right name and the right type, in the right place. A static rule saying that this attribute means nothing unless another attribute is also present would be wrong in general. It holds only for this pair, on this kind of node.

I could find the nine because the correct pattern already existed in the same package: Icon always pairs its label with role="img". Once I had seen the working case, I could search the code for the broken ones instead of inspecting every component.

This one became a verification rule. A label without a role now fails the build.

02

Props the types accept and the component drops

A component takes Omit<BoxProps, 'as' | 'ref'>. That leaves id, a11yRole, accessibilityLabel and pointerEvents in the public contract, so the types tell callers they may pass them, and callers do.

But a branch that doesn't destructure them explicitly passes them along in ...box to a style helper that returns only $-prefixed props. The helper consumes the props and drops them, and they never reach an element.

The tools stay quiet because an omitted prop is undefined, and undefined is a valid value for an optional prop. Nothing was typed wrongly, so there is no type error. The contract only promised that the prop would be accepted.

A type system doesn't model the gap between accepting a prop and using it. Whether a prop gets used depends on the implementation at runtime, and a signature has no way to express that.

I had four sibling components with three different subsets of these props wired. One passed all four props through, two passed two each, but not the same two, and one passed none. They shared a base type and an intent, behaved in four different ways, and every one of them typechecked.

I found it by comparing the siblings. Read alone, any one of them looks correct. Read side by side, the inconsistency shows in seconds.

03

Comments that describe an earlier version of the system

A comment mentions a limitation. Later the limitation is removed, and the comment stays.

Of the three, this is the one I'd warn people about most, because the code behaves correctly and the damage is in what developers come to believe. 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, and no one ever finds out.

There is a worse variant. During an edit, a JSDoc block shifted by one prop, so one prop carried two descriptions and its neighbor carried none. That is bad enough in the editor. In my system the docs are generated from source into the catalog, so the wrong description became the public documentation of that prop.

No tool catches this because it's a comment, and nothing in a JavaScript toolchain checks whether prose is true. Linters check that JSDoc is well-formed but can't tell whether it describes what the code does.

The other two classes have a shape you can search for. This one requires knowing what the system does now and comparing that with what the comment claims, and only reading the code tells you that. Of the three, it is the one that stays a human problem.

04

The questions no gate asks

Each automated gate answers one question. Typecheck asks whether the values fit, lint asks whether the syntax follows the rules, and the build asks whether the code compiles.

None of them asks whether an attribute does anything, whether a prop reaches an element, or whether a sentence in a comment is still true. Those are questions about what the code means, and no tool in the chain checks them.

05

What I changed

Two of the three became verification rules. Besides the rule for labels without a role, every prop declared in a contract must now be traceable to an element. The third didn't become a rule, because there is nothing for a rule to check.

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

I should also have done the review at 30 components instead of 75. 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.

Your causes will be different from these three. What carries over is that a review which stops at the findings has done half the work. The other half is tracing the findings back to their causes, and it gets more expensive with every component built on the broken pattern.

Where this came fromReviewing the cross-platform design system I built for Next.js, React Native and a Vite SPA.Read the case study →Related serviceAccessibilityView service →Next postTesting a design system on a real page: 7 components and 8 gapsRead →

Writing

If you’re working on something similar, send us the details.

The first call takes 30 minutes, and you’re under no obligation.

Tell us about your projectcontact@microtech.consulting