1.6 When TypeScript Helps and When It Gets in the Way
Look, let’s get this out of the way: TypeScript is not a magic wand. It won’t turn your spaghetti code into a Michelin-star meal. It’s more like a brilliant, slightly pedantic sous-chef who won’t let you add cinnamon to the bolognese without a very, very good reason. Most of the time, this saves you from culinary disaster. Sometimes, you just want to make a weird pie and you have to argue about it.
The value isn’t in just having types; it’s in having correct types. And the pain isn’t from the typing itself; it’s from fighting the type system when your mental model of the code (which is probably right) doesn’t match its overly strict, and sometimes hilariously wrong, model.
The “Oh Thank God” Moments: Catching the Dumb Stuff
This is TypeScript’s bread and butter. It exists to prevent the classic JavaScript face-palm moments. You know the ones. You call a function getUserDetails() expecting an object, and today, for some reason, it’s returning null. And then your whole app explodes because you tried to access userDetails.name on null.
TypeScript forces you to confront this reality before you ship the code.
// Without TypeScript, this is a ticking time bomb
function getLuckyNumber(user) {
return user.preferences.luckyNumber; // 💥 if user.preferences is undefined
}
// With TypeScript, you're forced to be honest
interface User {
preferences?: { // The '?' means this property might be undefined
luckyNumber: number;
};
}
function getLuckyNumber(user: User): number | undefined {
// Now you have to handle the possibility. The compiler will yell at you if you don't.
return user.preferences?.luckyNumber;
}
const num = getLuckyNumber({}); // num is `undefined`, not a runtime error.
The beauty here is the compiler’s nagging. It transforms a runtime error, which your user sees, into a compile-time error, which only you see. This is an unalloyed good.
The “Ugh, Fine” Moments: Dealing with the Outside World
This is where the pristine world of your typed code smashes into the messy reality of APIs, user input, and untyped libraries. You have to coerce this messy data into your clean type system, and this is where most developers get sloppy, defeating the entire purpose.
The biggest pitfall is just assuming the data is what you think it is. Don’t just cast it with as. That’s like putting a “GULLIBLE” sign on your own back. Validate it.
// ❌ The "I believe everything" approach (DANGER)
const data = await response.json() as MyExpectedInterface; // You hope. You pray.
// ✅ The "Trust, but verify" approach (CORRECT)
// Use a library like Zod or io-ts to validate the shape at runtime
import { z } from 'zod';
const MySchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const parseResult = MySchema.safeParse(await response.json());
if (!parseResult.success) {
// Handle the bad data gracefully. Log it. Throw a nice error.
throw new Error(`API returned malformed data: ${parseResult.error}`);
}
const data = parseResult.data; // data is now fully typed and VERIFIED as MyExpectedInterface
Yes, it’s more code. It’s also correct code. The TypeScript compiler can’t reach into your running program to check an API response, so you have to do it yourself.
The “What Were They Thinking?!” Moments: Imperfect Typings
Sometimes, you’ll use a library whose type definitions are just… wrong. Or overly broad. A classic example is the event object in a React handler. The type is React.ChangeEvent<any>, which is basically a polite way of saying “we give up, it’s any”.
Your job is to rein it in.
// The library's weak typing
const handleInputChange = (event: React.ChangeEvent<any>) => {
const value = event.target.value; // value is typed as `any` 😑
};
// Your superior typing
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value; // value is now typed as string 🎉
};
You’re not overriding the library’s types; you’re providing more specific generic parameters. This is a best practice. Never accept any when you can possibly narrow it down.
The “This is Absurd” Moments: Overzealous Correctness
This is where you have to remember that TypeScript is a superset of JavaScript, not a replacement. It has to be able to represent all the insane, dynamic, weird things you can do in JS. This leads to some frustrating moments.
A common one is Array.filter. You’d think filtering out all null values would give you an array of non-null values, right? Nope.
const maybeNumbers: (number | null)[] = [1, 2, null, 4];
const goodNumbers = maybeNumbers.filter(x => x !== null);
// TypeScript still thinks goodNumbers is (number | null)[]
// It can't statically prove the callback removes all nulls. It's technically correct, but infuriating.
// The fix: use a type guard
const isNotNull = <T>(x: T | null): x is T => x !== null;
const goodNumbers = maybeNumbers.filter(isNotNull); // goodNumbers is now number[]
You have to help the compiler help you. It’s a partnership, not a dictatorship. You provide the runtime logic and the type-level proof that the logic does what you say it does. It feels like overkill until you need to prove a complex condition, and then it’s genius.
The key takeaway is this: TypeScript is a tool for thought. It makes you structure your code more deliberately. The friction you feel isn’t pointless bureaucracy; it’s the system forcing you to consider edge cases and state possibilities you might have glossed over. Embrace the friction when it prevents errors. Learn to work around it gracefully when it’s being obtuse. And never, ever use as any to shut it up. That’s just unplugging the fire alarm because the noise is annoying.