DEV Community

Cover image for 55 Places My Code Predates the Language. All Auto-Fixed.
Ofri Peretz
Ofri Peretz

Posted on Originally published at ofriperetz.dev

55 Places My Code Predates the Language. All Auto-Fixed.

const latest = baseline[baseline.length - 1] ?? null;
Enter fullscreen mode Exit fullscreen mode

Nothing is wrong with that line. It passes review, ships, and works. It is also how you wrote JavaScript before 2022, and I have written it a thousand times since.

I ran four modernization rules over 389 .ts/.tsx files across four of my own repos. 55 findings in 36 files — and every single one carried an autofix. After --fix, the remaining count was zero.


The two that fired

prefer-at — 11 findings. The end-of-array idiom, mechanically rewritten:

// before
const latest = baseline[baseline.length - 1] ?? null;
for (const a of accrual[accrual.length - 2].articles) {
  /* … */
}

// after --fix
const latest = baseline.at(-1) ?? null;
for (const a of accrual.at(-2).articles) {
  /* … */
}
Enter fullscreen mode Exit fullscreen mode

prefer-template-literal — 44 findings:

// before
String(res.stderr || res.stdout || "claude exited " + res.status);

// after --fix
String(res.stderr || res.stdout || `claude exited ${res.status}`);
Enter fullscreen mode Exit fullscreen mode

Why this survives review forever: a reviewer's job is to reject broken code, and none of this is broken. arr[arr.length - 1] is correct in every runtime that ever existed. The only thing that can flag it is a tool that knows what year it is.

The two that found nothing

no-instanceof-array and prefer-event-target returned 0 findings across all 389 files.

A rule that never fires is not broken — it is a rule for a pattern you do not have. instanceof Array breaks across realms; if you have never hit it, silence is correct. The failure mode to fear is the opposite: I have a rule elsewhere that flags every .map() in JSX, 476 findings of noise. Yield tells you nothing about quality on its own — the same trap as counting rules instead of measuring them.

Lint as codemod, not as style

Most lint rules ask you to decide something. These ask you to apply something — the semantics are identical and the fixer is exact.

You do not triage 55 findings — you run the fixer once, read the diff as one commit, and the rule holds the line. A migration plus a ratchet, the same shape as an autofix turning a hardcoded secret into a one-command repair.

Check the diff, though — "auto-fixable" means exact, not invisible, and there are two catches. .at() is typed at(index: number): T | undefined, while arr[arr.length - 2] is typed T, so a chained access can stop compiling: my accrual.at(-2).articles above is TS2532: Object is possibly 'undefined' under --strict. The ?? null on the first example absorbs it; a bare chain needs a guard. And .at() is ES2022 — Node 18+ and any 2023+ browser are fine, older targets need a polyfill.

The config

// eslint.config.mjs
import modernization from "eslint-plugin-modernization";

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    plugins: { modernization },
    rules: {
      "modernization/prefer-at": "error",
      "modernization/prefer-template-literal": "error",
      "modernization/no-instanceof-array": "error",
      "modernization/prefer-event-target": "error",
    },
  },
];
Enter fullscreen mode Exit fullscreen mode
npm  install --save-dev eslint-plugin-modernization   # npm
yarn add     --dev      eslint-plugin-modernization   # yarn
pnpm add     --save-dev eslint-plugin-modernization   # pnpm
bun  add     --dev      eslint-plugin-modernization   # bun

npx eslint . --fix
Enter fullscreen mode Exit fullscreen mode

Measured against 3.1.2, peer range ^8.40.0 || ^9.0.0 || ^10.0.0, Node 18+. prefer-template-literal does not exist before 3.x, so an older install rejects the config. ESLint only.

All four at error is safe for these findings, but be precise why: in 3.1.2 only prefer-at and prefer-template-literal carry a fixer. All 55 findings came from those two, which is why --fix emptied the list — not because the plugin is 100% auto-fixable:

node -p "Object.entries(require('eslint-plugin-modernization').rules)
  .map(([k,r]) => k + ': ' + (r.meta.fixable || 'no fixer')).join('\n')"
# no-instanceof-array: no fixer
# prefer-at: code
# prefer-event-target: no fixer
# prefer-template-literal: code
Enter fullscreen mode Exit fullscreen mode

Run it against the published package: a monorepo symlink resolves to unreleased source and answers a different question. prefer-event-target gains a fixer after 3.1.2.

Rule docs · npm.


Numbers measured 2026-08-12 against four repos I own — my code, not a public corpus, so treat 55 as a shape.

Re-run 2026-09-04 over the 189 .ts/.tsx files of this blog's public apps/blog/src, plugin at 3.1.2: 8 findings in 6 files — six prefer-at, two prefer-template-literal. Three weeks after the codemod, the old idiom had crept back eight times. That is the argument for leaving the rules at error.

Two guards on it. The harness reports unmatched: 0, so all 189 files were configured — an earlier run returned a confident 0 that was 189 files matching no config. And on that tree the same day, the noisy rule above returns 110: yield is not quality.

What's the oldest idiom still alive in your codebase — and is it there because it's correct, or because nothing ever flagged it?

Top comments (1)

Collapse
 
mihai_leanzero profile image
Mihai Perdum

The TS2532 story is only half the picture depending on the tsconfig. I checked it with noUncheckedIndexedAccess on: arr[arr.length - 2] is already typed T | undefined under that flag, same as arr.at(-2). So for a project that already has it enabled the migration doesn't buy any new type safety at all, it's purely the readability/semantic win you're arguing for. It only introduces a new possibly-undefined error, the way your accrual example does, on projects without that flag, where the array access was silently typed T and .at() is the first thing that ever tells the compiler the truth.

Also curious what the actual bug was behind "an earlier run returned a confident 0 that was 189 files matching no config" - was that a files glob issue in flat config, or something with how overrides/ignores interact? That's exactly the kind of thing that looks like a clean pass and is actually silence.