Discipline
We banned any, as, is and ! from our TypeScript
TypeScript's safety has a peculiar property: it's opt-out. The language ships four convenient little doors out of the type system, and every one of them is used thousands of times a day in production codebases to make red squiggles go away. In TypeFirst codebases, all four are lint errors, CI runs with --max-warnings=0, and there is no override comment culture. Here's why each one had to go.
any — the lie that spreads
function getUser(): any { … }
const user = getUser()
user.naem.toUpperCase() // compiles. crashes at runtime. any is not "unknown type" — it's "skip checking, and skip checking everything this value touches." It's viral: one any return type infects every variable it's assigned to, every property accessed on it, every function it's passed through. A single any in a popular utility can silently un-typecheck a quarter of your codebase. TypeScript already has the honest version: unknown, which you must narrow before use.
as — the assertion nobody verifies
const user = (await res.json()) as User
// the server renamed `name` to `fullName` last sprint.
// this still compiles. it will compile forever. as tells the compiler "trust me." But the compiler was never the problem — reality was. The JSON didn't change because your annotation says so. Every as is a claim that's checked zero times, at the exact place (a system boundary) where data is most likely to surprise you. The honest version is to decode: parse the unknown value with a runtime validator that returns a typed Result — we wrote a whole lesson on it.
is — the predicate the compiler takes on faith
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null
// forgot to check .name and .email — compiler doesn't care
} Custom type guards look rigorous, but the body of an is function is unchecked: return true for the wrong thing and TypeScript will cheerfully narrow to a type the value doesn't have. It's as wearing a lab coat. Built-in narrowing — typeof, switch on a discriminant field, == null checks — does the same job and is actually verified by the compiler.
! — optimism as an operator
const el = document.querySelector(".profile")!
// narrator: it was null The non-null assertion is a runtime crash you schedule in advance. null is information — "this might be absent" — and ! deletes the information instead of handling it. The honest version is two lines: check for null, decide what absence means here. If absence is truly impossible, that's a fact your types should express (make the impossible state unrepresentable) rather than a fact you assert at every use site.
What we do instead
- Boundary data (HTTP, storage, env vars, URLs): decode
unknownwith runtime decoders into known types. Bad data fails loudly at the door, once. - Narrowing: discriminated unions with a
_ttag and exhaustiveswitch— the compiler verifies every branch. - Possibly-absent values:
Maybe<T>(T | null) and explicit handling, orResult<E, T>when the caller needs the reason — see Never throw. - Validated values: opaque types with smart constructors, so "this string is a real email" is a type, not a hope.
What actually happened
The first two weeks are slower. The escape hatches exist because they're convenient, and without them you must learn the honest pattern for each situation — that's exactly the curriculum our Learn series teaches. Then it flips:
- Refactoring became mechanical. With no lies in the types, "change the type and fix every red squiggle" is a complete, trustworthy procedure. There is no class of bug hiding behind an
asthat compiles anyway. - Code review changed subject. Nobody argues about whether a cast is safe — there are no casts. Reviews discuss domain modeling instead.
- Runtime type errors approached zero. Not all bugs, of course — logic bugs survive. But the
cannot read property of undefinedfamily died almost entirely, because every place it used to breed is now a compile error.
The rule only works because it's absolute. Allow "just this one justified as" and a year later you have hundreds, each one individually reasonable, collectively making your types untrustworthy again. Zero is the only number that's easy to enforce — and the compiler is the only reviewer that never gets tired.