Every TypeScript argument eventually reaches the same person: the one who says it's 'just JavaScript with extra steps.' And sure — if you also think a seatbelt is just a chair with extra steps. The steps are the point.
TypeScript's entire pitch is a trade: a little friction *now*, while you're writing the code and have full context, in exchange for not getting paged at 3am because something was undefined in a way you swore was impossible. Yell at me now, or page me later. I know which one I'm picking.
Types are documentation that can't lie
Comments rot. They describe what the code did six months ago, before three people changed it and none of them updated the comment. Types can't rot, because the compiler refuses to let them. A function signature is documentation the build *enforces*.
// a comment hopes you pass the right thing
function charge(amount, currency) { /* ... */ }
// a type guarantees it — and your editor autocompletes the options
function charge(amount: number, currency: 'USD' | 'EUR' | 'INR'): Promise<Receipt> {
// pass 'usd' lowercase? compiler stops you. pass a string? stopped.
}The compiler is autocomplete that actually knows things
Once your data is typed, your editor stops guessing and starts *knowing*. It autocompletes fields that exist, refuses ones that don't, and follows the shape of your data three levels deep. Half of 'being fast in a codebase' is just the editor knowing what's available — and that only happens when the types are real.
any is a confession
Every any is you telling the compiler 'trust me, I've got this' — and the compiler, relieved, stops checking entirely. One any quietly spreads: anything it touches becomes any too, and now you've paid for TypeScript and turned it off in the exact spot most likely to break.
// the lazy version: now `user.nmae` (typo) compiles fine. good luck.
function handle(user: any) { return user.nmae.toUpperCase(); }
// the honest version: `unknown` forces you to actually check before you trust
function handle(user: unknown) {
if (isUser(user)) return user.name.toUpperCase(); // narrowed, safe
throw new Error('not a user');
}Tip
Reach for unknown instead of any when you genuinely don't know a type. unknown makes you prove what it is before you use it; any just lets you lie.
Let the types do the thinking
The real unlock is modeling your domain so that *illegal states can't be represented*. If a request can be loading, loaded, or errored — but never loading-and-errored — a union type makes the impossible combinations literally impossible to write.
// don't: three booleans, eight states, five of them nonsense
type Bad = { loading: boolean; error?: string; data?: Result };
// do: a union — only the valid states exist, and TS forces you to handle each
type State =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'ok'; data: Result };Good types don't just catch bugs. They make whole categories of bug impossible to express in the first place.
The friction is the feature
Yes, sometimes the compiler yells at you and you mutter and add a check. That check is not busywork — it's a bug you would have shipped, surfaced before it could hurt anyone. The annoyance you feel is the sound of a problem being caught at the cheapest possible moment.
Generics aren't scary, they're just blanks
Generics scare people because of the angle brackets, but the idea is simple: a generic is a type with a blank you fill in later. Array<T> is 'an array of *something*' — you tell it the something. Write a function once, keep full type safety for every type that flows through it.
// without: you lose the type, everything becomes `any` on the way out
function first(arr: any[]) { return arr[0]; }
// with: T is a blank — pass strings, get a string back; pass users, get a user
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first(['a', 'b']); // string | undefined
first([user1, user2]); // User | undefinedWhere to draw the line
You can absolutely go too far — type gymnastics so clever the error messages need their own translator. Don't. The goal isn't a perfect type system; it's catching real bugs cheaply. If a type takes longer to write than the bug would take to fix, you've over-engineered it. Pragmatism beats purity.
JavaScript trusts you completely, which is lovely right up until you make one human mistake at scale. TypeScript trusts you less and saves you more. Let it be pedantic now so production can be boring later.
Key takeaways
- 01TypeScript trades a little friction now for not getting paged at 3am later.
- 02Types are documentation the compiler enforces — they can't go stale like comments.
- 03Treat `any` as a code smell; prefer `unknown` and model illegal states out of existence.
FAQ
Related reading