TypeScript Made Me a Better Designer
This is not a TypeScript post. It is about what strict type thinking does to the way you see interfaces.
When I started using TypeScript properly — strict mode, no any, modeling domain objects carefully — something shifted in how I looked at UI. I started asking a question I hadn't asked before: what are the valid states of this?
The question
A button. What are its valid states?
The obvious ones: default, hover, active, focus, disabled. You probably have those in your design system.
But what about: loading? What does a button look like while the form it submits is waiting for an API response? Most designs don't specify this. The button just sits there, maybe with a spinner if someone remembered to add it. The user clicks again. A second request fires. The UI handles this poorly or not at all.
TypeScript would not let this happen. You can't forget a state in TypeScript — if your union doesn't handle 'loading', the compiler tells you. You must deal with all cases.
Exhaustive design
The concept in TypeScript is called exhaustive matching. When you have a discriminated union and you switch on its type, TypeScript checks that you've handled every variant. If you miss one, the code doesn't compile.
Design doesn't have a compiler. But the thinking transfers.
Before I design a component, I list its states as a union:
state: 'idle' | 'loading' | 'success' | 'error' | 'disabled'
Then I design all of them. Not all at equal fidelity — but all intentionally. The error state isn't an afterthought. The empty state isn't a placeholder. Each variant is a first-class design decision.
The quality of a design system is largely determined by how thoroughly it handles the boring states — loading, empty, error, skeleton. Anyone can design the happy path. The real work is in the variants.
What this does to component APIs
When I design a component for someone else to use — whether "someone else" is a teammate, a client's developer, or future me — I think about what the API should look like.
A well-typed component tells you what's possible and enforces the contract. It can't be used wrong in the ways the type system covers.
This is a design problem, not an engineering one. The interface of a component — what props it accepts, what it refuses, what it requires — is a UX decision for the developer using it. A prop called variant that accepts 'primary' | 'secondary' | 'ghost' is clearer than one that accepts a string. The narrower type is the better design.
I started thinking about component APIs the same way I think about user-facing interfaces: what is the minimum set of decisions the user needs to make? What should be impossible? What should have a sensible default?
The broader principle
TypeScript made me see that correctness is a design dimension. A form that submits with an empty required field is not just a validation failure — it's a design failure. The design didn't make the invalid state impossible.
Good design, like good types, prevents mistakes at the boundaries rather than recovering from them inside. Guard at the input. Make the wrong state unrepresentable.
This is also why I care about empty states, loading states, and error states at least as much as the happy path. They are not edge cases. They are the normal experience of any interface that touches the real world — which is all of them.
The TypeScript mindset is: every state the system can be in must be accounted for, and the failure to account for it is a bug. Applied to design: every state the interface can be in must be designed for, and the failure to design it is a gap someone else will fill with something worse.