title: "When to break the type system"
date: 2024-11-22
tags: [engineering]
reading_time: 5 min
slug: when-to-break-the-type-system
---
When to break the type system
There's a category of TypeScript user who treats as any like an unforgivable sin. They will spend a full afternoon wrestling a generic constraint to avoid one. That user is usually me, and I'm increasingly convinced I'm wrong about it.
What the type system is for
The type system exists to prevent a specific class of bugs: wrong shape at call sites, wrong assumption about what a value can be. That's a job it does well.
It is not a substitute for tests. It is not a guarantee of correctness. It does not stop runtime data from being garbage. If you've ever parsed a JSON response and declared it User, you've told the compiler a story that might be a lie.
The honest escape hatches
TypeScript gives you three reasonable ways out:
as X— "trust me, this is X"X | unknown— "this is X or I don't know, caller checks"@ts-expect-error— "this line is wrong, and I know it"
All three are fine. @ts-expect-error is underused. Unlike @ts-ignore, it complains when the underlying issue is fixed — so it self-cleans.
When to use them
I reach for escape hatches when:
- An external library's types are wrong, and upstreaming a fix would block shipping
- A migration is mid-flight and the correct type is three refactors away
- Narrowing would require a helper function whose only purpose is to appease the compiler
I don't use them when:
- I don't understand why the type error is happening (that's the compiler doing its job)
- The fix is actually five minutes away
The real cost
The cost of as any isn't in that line — it's in the next person who has to modify the code around it. If I leave a note why, most of that cost disappears:
// upstream type is incorrect, PR: library/foo#123
const user = res.data as User
A 25-character comment buys you a lot of forgiveness from future-you. Use it.