# The TypeScript mistakes language models actually make

> Source: https://learn-typescript.org/review/failure-modes/
> Part of Learn TypeScript, free to read.

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`.

```bash
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](/ai/types-as-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](https://learn-javascript.org/review/failure-modes/) and applies unchanged.

## Idioms worth correcting

| Generated | Prefer |
|---|---|
| `enum Status { ... }` | a union of string literals, or `as const` — enums emit runtime code and are not erasable |
| `namespace` | modules |
| `interface` vs `type` inconsistently | pick one convention and put it in AGENTS.md |
| `Function`, `Object`, `{}` as types | specific signatures; `{}` means "anything not null" |
| `require()` in an ESM project | `import` |
| custom `DeepPartial`, `Awaited` | built-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

```json tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "verbatimModuleSyntax": true,
    "erasableSyntaxOnly": true
  }
}
```

```js 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.

:::verdict 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.
:::

:::promo frontendmasters
:::

## 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.
