TypeScriptNov 7, 2026 · 6 min read

i banned `as` with one eslint rule

For years, my answer to a TypeScript error was as. Red squiggle under a value, I added as Something, the squiggle went away, and I moved on. It felt like fixing the problem. It was not fixing the problem, it was telling the compiler to stop looking at it (which, to be fair, is exactly what I wanted at 1am).

The habit caught up with me on a seating chart app. I was loading a saved guest list from localStorage, parsing it with JSON.parse, and casting the result to my Guest type. It worked in dev, every time, because my dev data was clean. Then a real user had an old saved list from an earlier version of the app, before I added a field, and the page went blank. The cast had promised the compiler the data was a Guest. It was not.

what as actually does

as is not a check. It is a promise you make to the compiler, and the compiler takes your word for it. It checks that the claim is plausible, that the two types have something in common, but it does not check that the value actually fits. If the value is wrong, you find out at runtime, in front of a user, which is the worst place to find out anything.

guests.ts (the lie)TypeScript
type Guest = { name: string; table: number };

const stored = localStorage.getItem('guests');
const guests = JSON.parse(stored) as Guest[];

guests[0].name.toUpperCase(); // crashes if the saved data is stale

The same thing happens with a plain object. A typo in a key compiles fine under a cast, because a cast skips the excess property check that would normally catch it.

theme.ts (the typo that compiled)TypeScript
type Theme = { primary: string; accent: string };

const theme = {
  primary: '#2F6B4F',
  accnet: '#C9A227',
} as Theme;

theme.accent.toUpperCase(); // undefined at runtime, no error at compile time

the one rule

I wanted the habit gone, not just noticed, so I put one rule in my eslint config and let it do the nagging for me.

eslint.config.jsTypeScript
import tseslint from 'typescript-eslint';

export default tseslint.config({
  rules: {
    '@typescript-eslint/consistent-type-assertions': [
      'error',
      { assertionStyle: 'never' },
    ],
  },
});

That is the whole change. One rule, and every as in the codebase turns into an error. The first run flagged casts in files I had not touched in months, most of them in the exact spots where I had been sloppy at 1am (which tracks).

Two things the rule leaves alone, and both are fine by me. as const still works, the rule ignores const assertions completely, so my literal types stay put. And casts to any are ignored too, which sounds like a loophole until you realize casting to any is its own problem, and a different rule's job.

what i reach for instead

Banning the shortcut only works if there is something better to reach for. There are three, and they cover every as I had.

First, a plain annotation. const theme: Theme = ... checks the value against the type, and a typo becomes a compile error instead of a blank page. It widens literal types to the declared type, which is fine when the variable is meant to hold any value of that type.

Second, satisfies, which landed in TypeScript 4.9. It checks the value against the type without widening it, so you keep the precise literal types you wrote. This is the one I reach for with config objects and theme objects, the exact places I used to cast.

theme.ts (checked, not cast)TypeScript
const theme = {
  primary: '#2F6B4F',
  accent: '#C9A227',
} satisfies Theme;

// theme.primary is still the literal '#2F6B4F', not just string
// a typo like accnet errors here, at the satisfies line

Third, narrowing unknown for data that comes from outside, like parsed JSON. Instead of casting the result, I check it with a type guard and let the check do the narrowing.

guests.ts (checked, not cast)TypeScript
function isGuest(value: unknown): value is Guest {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    typeof value.name === 'string'
  );
}

const data: unknown = JSON.parse(stored);
if (isGuest(data)) {
  console.log(data.name);
}
Live: trust the cast, or check first

Press a button.

The demo is the whole post in two buttons. The first one trusts the cast: the value is a number wearing a string costume, and calling a string method on it throws. The second one checks first with typeof, and the wrong type turns into a handled case instead of a crash. Same value, same line of code, different ending.

the honest exception

A couple of places still needed a cast, mostly where a library's types were wrong and the value was genuinely something the types could not express. For those, I left the cast in and put an eslint-disable-next-line above it with a comment saying why. That is the whole point, really. The exception is visible, it has a reason, and the next person to read it (usually future me) can see the reasoning instead of tripping over a silent as.

Anyways, the rule did not make me a better TypeScript developer. It made me stop reaching for the shortcut, and the compiler started doing the job I hired it for. The bugs it catches now are the ones I used to ship, and the code reads more honestly: when a value is checked, you can see the check, and when it is a guess, you can see that too. That trade was worth one line in a config file.