Review & Verify Updated 2026-09 9 min read View as Markdown

The TypeScript mistakes language models actually make

The compiler catches a lot. What survives is almost always the compiler being told to look away.

TypeScript's failure modes are unusual: the language has a good static checker, so the interesting bugs are the ones where the checker was disabled, explicitly or by a loose config.

That makes review here mostly mechanical. There is a short list of escape hatches, they are all greppable, and each one is a place where a claim was made that nothing verified.

The escape hatches#

1. as on external data#

ts
const user = await res.json() as User;          // a claim, not a check
const config = JSON.parse(raw) as Config;
const el = document.querySelector(".x") as HTMLInputElement;

as is an assertion. Nothing runs. If the API changed a field name last week, the type still says it did not, and the failure surfaces three functions away as undefined is not an object.

Correct: parse with a schema and let the type flow out of it.

ts
const User = z.object({ id: z.string(), email: z.email() });
const user = User.parse(await res.json());

2. Non-null assertions#

ts
const first = items.find(x => x.id === id)!;    // "trust me"
process.env.API_KEY!.slice(0, 4);

Each ! is an unchecked claim, and generated code produces them whenever narrowing would take an extra line. Catch it with: @typescript-eslint/no-non-null-assertion.

3. any, explicit and implicit#

any disables checking for everything it touches and spreads through inference. Generated code reaches for it under pressure — an awkward generic, a library without types.

Correct: unknown plus a narrowing check. It is one more line and it does not spread.

Catch it with: no-explicit-any and no-unsafe-assignment / no-unsafe-member-access / no-unsafe-call from the type-checked preset. Those three catch any arriving from an untyped dependency, which is where most of it comes from.

4. @ts-ignore#

ts
// @ts-ignore
doTheThing(wrongArgs);

Correct: @ts-expect-error, which errors if the line stops being wrong — so it cannot rot silently. Ban @ts-ignore outright with ban-ts-comment.

shell
git diff | grep -nE ' as [A-Z]| as any|: any|@ts-ignore|!\.|!\)|!;'

That one command is most of a TypeScript review.

Unsoundness the compiler permits by default#

5. Array and record access#

ts
const first = items[0];          // typed T, actually T | undefined
const port = config["port"];     // same

TypeScript's default is unsound here for ergonomic reasons, and generated code assumes presence constantly. Fix it with: noUncheckedIndexedAccess: true. This is the single highest-value compiler flag for generated code.

6. Optional properties versus undefined#

ts
type Opts = { retries?: number };
const o: Opts = { retries: undefined };   // allowed by default. usually a bug.

Fix it with: exactOptionalPropertyTypes: true.

7. Structural typing surprises#

ts
function transfer(from: string, to: string, amount: number) {}
transfer(orgId, userId, amount);          // compiles. wrong.

Three strings are interchangeable because they are three strings. Fix it with: branded types — see the type system as a harness.

8. Widened literal types#

ts
const config = { mode: "dark" };          // mode: string, not "dark"
setTheme(config.mode);                    // error, or worse, accepted as string

as const on config objects. Generated code omits it and then reaches for as to fix the symptom.

Async#

TypeScript inherits every JavaScript async failure — floating promises, sequential awaits, forEach with an async callback — and adds one of its own:

9. A promise where a value was expected#

ts
if (isReady()) { }                        // isReady returns Promise<boolean>
                                          // a Promise is always truthy

Catch it with: @typescript-eslint/no-misused-promises and await-thenable. The full JavaScript async list is in the JavaScript failure modes and applies unchanged.

Idioms worth correcting#

GeneratedPrefer
enum Status { ... }a union of string literals, or as const — enums emit runtime code and are not erasable
namespacemodules
interface vs type inconsistentlypick one convention and put it in AGENTS.md
Function, Object, {} as typesspecific signatures; {} means "anything not null"
require() in an ESM projectimport
custom DeepPartial, Awaitedbuilt-in utility types now cover most of it

The enum point matters more than it used to: with erasableSyntaxOnly and runtime type stripping, enums and parameter properties are no longer erasable syntax. Generated code still reaches for them constantly.

The config#

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "verbatimModuleSyntax": true,
    "erasableSyntaxOnly": true
  }
}
eslint.config.js
rules: {
  "@typescript-eslint/no-explicit-any": "error",
  "@typescript-eslint/no-non-null-assertion": "error",
  "@typescript-eslint/no-unsafe-assignment": "error",
  "@typescript-eslint/no-unsafe-member-access": "error",
  "@typescript-eslint/no-unsafe-call": "error",
  "@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": true }],
  "@typescript-eslint/no-floating-promises": "error",
  "@typescript-eslint/no-misused-promises": "error",
  "@typescript-eslint/consistent-type-assertions": ["error", { "assertionStyle": "never" }],
}

That last rule — banning as entirely — is aggressive and worth trying. In most codebases the legitimate uses are rare enough to justify with an explicit eslint-disable comment, and forcing that comment is exactly the review prompt you want.

The whole review, compressed

Turn on the four strict-adjacent flags. Ban the escape hatches in lint. Then the only TypeScript-specific thing left to read for is whether the types describe the right thing — which is judgement, and is where your attention should go.

Common questions#

Is banning as outright practical?#

More often than people expect. Genuine uses — narrowing a DOM element, as const, a well-understood cast at a library boundary — are rare enough that requiring an explicit disable comment is reasonable, and the comment is a useful review signal.

Why does generated code use enums when the ecosystem moved away from them?#

Because enums appear throughout a decade of TypeScript in the training data. They also now conflict with runtime type stripping, since they emit real JavaScript. A union of string literals does the same job, is erasable, and narrows better.

Do these rules slow the build down?#

The type-aware rules need a TypeScript program, so linting is slower — noticeably on a large repo. Use projectService and lint changed files in the edit hook, full repo in CI. The bugs they catch are worth the seconds.

Get the TypeScript agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for TypeScript. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.

Disclosure: some links on this page are affiliate links. If you buy something through one, we earn a commission at no extra cost to you. We only list tools we would tell a friend to use, and we say so when we have not used something ourselves. This is how the site stays free and ad-light.