· 3 min read
Types considered harmful
TypeScript becomes dangerous when you silence honest absence with plausible defaults that satisfy the shape and corrupt the meaning.

Types are not the harmful part. Treating a type error as paperwork is. The checker asks you to handle missing information, and you answer by manufacturing information that fits the interface.
type Account = { id: string; email: string; plan: "free" | "pro";};function accountFor(id: string): Account { return accounts.get(id) ?? { id, email: "", plan: "free", };}This is where type safety turns into theater. The lookup said no account exists, and the function papered over that answer with a customer who has no email and a plan nobody selected. The annotation is satisfied. Everything downstream now treats an account the business never created as real.
The checker is not a witness#
TypeScript's strictNullChecks option makes null and undefined distinct types because a lookup may genuinely fail. That warning is useful evidence. It tells you the world did not produce the value your function promised.
undefined was the last honest value in the program.The checker will happily verify that your fallback carries an id, an email, and a valid plan, but it has no way to know whether the account exists. Once you invent a well-shaped object, TypeScript has nothing left to complain about.
Map.get
There is no account with this id.
TypeScript
Then your return type cannot be Account.
Developer
I invented a free account with an empty email.
Production
Why did we authorize a customer who does not exist?
Defaults are policy#
Every line below looks like cleanup, and every line makes a product decision.
const price = prices[sku] ?? 0;const retries = config.retries || 3;const country = profile.country ?? "US";const enabled = flags[feature] ?? true;A missing price becomes free. The retry line is worse than it looks, since || treats 0 as false and turns a deliberate zero into three. An unknown country becomes a jurisdiction, and an absent flag enables a feature nobody turned on.
That country line has already bitten me: this spring I traced a mis-taxed invoice to a profile.country ?? "US" that had been sitting in our checkout service since a refactor, assigning a jurisdiction to every account that predated the country field. I spent the first afternoon convinced the tax provider was broken, because a one-line fallback never looks like a suspect. As far as I can tell nobody ever chose that default. It arrived in a diff that was mostly renames, and it read as tidying.
The narrowing handbook lists 0 and the empty string among JavaScript's falsy values. The noUncheckedIndexedAccess option deliberately adds undefined to unknown indexed fields. Both exist to remind you that a key can be absent, which is a different instruction from “spray ?? 0 until the red goes away”.
Absence can carry meaning too. TypeScript's exactOptionalPropertyTypes option distinguishes a missing property from one explicitly set to undefined, because JavaScript can observe the difference. Flattening both into a convenient default erases a state your program may need later.
Defaults are fine when they are real policy: pageSize ?? 50 is honest if the product says omitted page size means fifty. A default should survive a conversation with the person who owns the behavior. “The compiler was red” is not a product requirement.
Keep the error alive#
Expected absence should stay in the return type. Impossible configuration should fail at the boundary. Both are better than sending plausible garbage deeper into the system.
type Lookup<T> = | { ok: true; value: T } | { ok: false; reason: "not-found" };function accountFor(id: string): Lookup<Account> { const account = accounts.get(id); if (account === undefined) { return { ok: false, reason: "not-found" }; } return { ok: true, value: account };}function requiredEnv(name: string): string { const value = process.env[name]; if (value === undefined) { throw new Error(`Missing environment variable: ${name}`); } return value;}The union forces the caller to choose what “not found” means. The configuration helper crashes where recovery is impossible, before an empty credential becomes an authentication mystery six services away. A non-null assertion or as Account is the same lie without even the courtesy of creating a runtime value.
“Make invalid states unrepresentable” only works when you stop inventing valid-looking inhabitants for invalid states. Behind every red underline on a possibly-missing value sits one question — what should happen when it is missing? — and the code at that line usually has no idea. Pass the question upward until something does.
“Considered harmful” gives types too much agency. All the checker did was ask whether an account exists. A person answered, the answer compiled, and somewhere past code review a customer with an empty email and a plan nobody picked is waiting for the first support ticket.