TypeScript

Fix 'The keyword enum is reserved' in ESLint + TypeScript

Why replacing react-app with airbnb drops the TypeScript parser Create React App quietly provided, and the one-line fix for both flat config and the now-removed .eslintrc format.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
9 min read

TL;DR#

ESLint's default parser, Espree, is built on Acorn, which has no concept of a TypeScript enum declaration. Acorn tokenises the bare word enum as a plain identifier, checks it against its own ECMAScript reserved-word list, finds it there, and raises the error below before it ever reaches the {. The fix is to point ESLint at @typescript-eslint/parser instead of the default — one line, whether the project uses the flat eslint.config.mjs ESLint 9 and 10 read, or the .eslintrc format ESLint 9 and earlier supported (ESLint 10 removed it outright — see the Fix section). Raising ecmaVersion does not help; it was reproduced below and the error does not move.

text
Parsing error: The keyword 'enum' is reserved

This is the exact message from the Stack Overflow question (10 votes, over 9,000 views): a Create React App project that linted its TypeScript fine until .eslintrc.json switched to extends: ["airbnb"]. Reproduced on 26 October 2026 on the host (Node.js v26.7.0), with eslint 9.39.2, @typescript-eslint/parser 8.63.0 and typescript 5.9.3 — the exact versions this reproduction installed, not necessarily the ones in the original question's project.

Why it happens#

ESLint doesn't parse JavaScript itself — it delegates to a parser, and the default one is Espree, which wraps Acorn. Neither knows what a TypeScript enum is, because enum Color { ... } is not ECMAScript syntax; it's a TypeScript-only extension, never part of the language, not merely newer than this parser. When Acorn's statement parser meets the word enum at the start of a statement, not after export (see the table and mechanism further down), it has nothing better to do with it than treat it as an identifier reference, so it calls the same function it uses to check any other identifier: checkUnreserved. That function tests the word against Acorn's own reserved-word list — and enum is on it. Acorn's source for the version this reproduction installed (8.15.0, pulled in by eslint@9.39.2 → espree@10.4.0 → acorn@^8.15.0) spells out the list directly:

text
export const reservedWords = {
  3: "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile",
  5: "class enum extends super const export import",
  6: "enum",
  strict: "implements interface let package private protected public static yield",
  strictBind: "eval arguments"
}

enum sits in the ECMAScript-6-and-above list on its own (6: "enum"), unconditionally — not only in strict mode. The moment checkUnreserved matches it, Acorn raises `The keyword '${name}' is reserved` — the literal source of the message this page is about. That's also why raising parserOptions.ecmaVersion never helps, however high: the grammar reserves the word, it just never shipped a feature that uses it, so there's no version where a plain parser accepts it. This was reproduced directly: with ecmaVersion: 'latest' and the default parser, the identical error came back on the same file. ESLint's own documentation on configuring a parser confirms Espree is the default and lists @typescript-eslint/parser by name as a parser known to work with ESLint.

Why this specific project hit it: the question itself says "I used react-app config for the .eslintrc.json" before switching to Airbnb, and eslint-config-react-app's own source carries a built-in override — files: ['**/*.ts?(x)'], parser: '@typescript-eslint/parser' — that quietly parses every .ts/.tsx file with the TypeScript-aware parser. Switching extends to ["airbnb"] alone drops that override: eslint-config-airbnb's own package metadata lists eslint-config-airbnb-base, object.assign and object.entries as dependencies and eslint plus four eslint-plugin-* packages as peer dependencies — no @typescript-eslint/parser anywhere — so ESLint falls back to Espree on the exact same files that used to lint cleanly.

Fix#

1. Install the parser (skip if already a dependency):

bash
npm install --save-dev @typescript-eslint/parser

2. Flat config (eslint.config.mjs, the default since ESLint 9 and, since ESLint 10, the only format ESLint reads at all — more on that below) — set the parser under languageOptions.parser, covering both .ts and .tsx:

js
import tsParser from '@typescript-eslint/parser';
 
export default [
  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      parser: tsParser,
      ecmaVersion: 2022,
      sourceType: 'module',
      parserOptions: {
        ecmaFeatures: { jsx: true },
      },
    },
  },
];

3. Legacy .eslintrc.json — the exact one-line fix from the 2020 accepted answer, reproduced below on ESLint 9.39.2 with a deprecation warning. Add the parser key at the top level:

json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2022,
    "sourceType": "module"
  }
}

On ESLint 8 and earlier this is the default format: nothing else to do. On ESLint 9, flat config is the default instead, so this file is only read with ESLINT_USE_FLAT_CONFIG=false set (the exact condition this reproduction ran under); without it, ESLint 9 looks for eslint.config.js and ignores this file. On ESLint 10, neither applies: ESLint 10.0.0's own release notes (published 6 February 2026) list a breaking change, feat!: remove eslintrc support, and 10.13.0 — not a 9.x release — is the latest tag on npm as of this reproduction, so step 2 is the only option there.

4. If the project uses eslint-config-airbnb specifically, check which ESLint it actually runs on first: eslint-config-airbnb 19.0.4's own peer dependencies are "eslint": "^7.32.0 || ^8.2.0" — it doesn't list ESLint 9 or 10 in that range at all, so a project that still has it installed is normally on ESLint 7 or 8, on the .eslintrc format by default there (step 3, no environment variable needed on 8 and earlier). Moving that project to flat config (step 2) fixes only the parsing error; step 2's block above contains none of Airbnb's actual rules, so the rest of them would need a separate migration, not just this one line. A separate, third-party package — eslint-config-airbnb-typescript, maintained independently of Airbnb by iamturns, not by Airbnb itself — sets the same parser: '@typescript-eslint/parser' key internally for the .eslintrc format (confirmed directly in its lib/shared.js, line 12). GitHub lists that repository as archived; its README is headed "This repo has been archived" and adds, in the maintainer's words: "After six years and reaching 2 million weekly downloads, I can no longer give this project the attention it deserves" — it points to eslint-config-airbnb-extended as an alternative. Installing a shareable config just for one parser line is optional either way: step 3 alone is enough to clear this error on the version this reader is actually running.

One more check before installing anything: the current @typescript-eslint/parser (8.72.1, and this reproduction's 8.63.0 alike) declares peer dependencies "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" — it covers neither ESLint 7.x nor any ESLint 8 release below 8.57.0, only 8.57.0 and later. A reader whose eslint-config-airbnb install keeps them below that may see a peer dependency conflict installing today's parser release; picking an older @typescript-eslint/parser release whose own peer range covers that ESLint version is the general way out of that kind of conflict (not reproduced here for this specific pair of versions).

5. Full TypeScript linting, not just parsing. The parser alone only stops the crash. For the complete, officially documented rule set, install the whole quickstart (this adds packages step 1 didn't, @eslint/js and the typescript-eslint meta-package):

bash
npm install --save-dev eslint @eslint/js typescript typescript-eslint

Then the typescript-eslint package's own flat-config quickstart — quoted here verbatim from its docs, not independently reproduced in this session:

js
// @ts-check
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
 
export default defineConfig({
  files: ['**/*.{js,ts}'],
  extends: [js.configs.recommended, tseslint.configs.recommended],
});

For a project with .tsx files too, the same docs widen files to ['**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}']. tseslint.configs.recommended does bundle the same parser fix as step 2: the installed @typescript-eslint/eslint-plugin 8.63.0 package's own dist/configs/flat/recommended.js composes base(plugin, parser) as its first entry, and dist/configs/flat/base.js is exactly languageOptions: { parser, sourceType: 'module' } — confirmed by reading both files directly in node_modules, not just the docs.

The install command above pins no versions, so it installs whatever latest is today for each package: ESLint 10.13.0 (consistent with a flat-config-only quickstart) and, checked on the npm registry 2026-10-26, typescript 7.0.2. @typescript-eslint/parser and @typescript-eslint/eslint-plugin 8.x declare "typescript": ">=4.8.4 <6.1.0" as a peer dependency — 7.0.2 is well outside that range. Pin typescript to a release the installed @typescript-eslint packages actually declare support for, such as the 5.9.3 this reproduction used, rather than running the quickstart's install line unpinned.

Verify the fix#

With a flat eslint.config.mjs (step 2 or 5), run ESLint the normal way — its own files glob decides what gets linted, so a directory scan already reaches every .ts/.tsx file:

bash
npx eslint .

Reproduced directly: scanning this reproduction's whole fixture directory with the fixed flat config reported zero problems on every enum/interface/type-annotation fixture (the only findings left were unrelated rule errors from other, unrelated .ts reference files sitting in that same folder).

With an .eslintrc.json on ESLint 9 (step 3), eslint . alone is not enough, even with the environment variable set. Reproduced: pointed at this reproduction's directory, the legacy engine skipped every .ts/.tsx fixture entirely and linted an unrelated .js file instead — not a pass, a file list that never included the files this page is about. The legacy engine's default scan is JavaScript files only; add --ext:

bash
ESLINT_USE_FLAT_CONFIG=false npx eslint . --ext .ts,.tsx

Reproduced with that flag added, same directory, same .eslintrc.json: every enum/interface fixture cleared, and only the same unrelated files kept their unrelated findings. On ESLint 8 and earlier, drop the environment variable — eslintrc is simply the default there — but --ext is still needed for the same reason.

Either way, once the right files are actually being scanned, the exact line that previously failed, Parsing error: The keyword 'enum' is reserved, no longer prints. A project with real lint rules enabled (Airbnb's, or typescript-eslint's own from step 5) will still print its normal rule findings, and step 3's ESLintRCWarning deprecation notice, afterwards; what disappears is only the Parsing error line itself.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

The 2026 mistake: pasting the old fix into a flat eslint.config.mjs#

The accepted answer's exact line, "parser": "@typescript-eslint/parser", is for .eslintrc. Flat config has no top-level parser key at all, so pasting that line as-is into eslint.config.mjs — instead of nesting it under languageOptions.parser as in step 2 — does not reproduce this page's error. It fails ESLint's own config validation first, with ESLint naming the right key:

text
Oops! Something went wrong! :(
 
ESLint: 9.39.2
 
A config object is using the "parser" key, which is not supported in flat config system.
 
Flat config uses "languageOptions.parser" to override the default parser.
 
Please see the following page for information on how to convert your config object into the correct format:
https://eslint.org/docs/latest/use/configure/migration-guide#custom-parsers

If a project is still on the legacy format, this doesn't apply — the plain "parser" string is correct there, and step 3 above reproduced it working, deprecation warning and all: ESLint 9 still accepts .eslintrc files with ESLINT_USE_FLAT_CONFIG=false set, logging ESLintRCWarning: ... support will be removed in v10.0.0 — and, as step 3 covers, ESLint 10 has since shipped and removed it outright, that environment variable included. Moving to flat config means moving the key, not just copying the line across.

Other TypeScript syntax fails the same way, with different wording#

enum is not the only TypeScript construct a default-parser project trips over, and the exact wording isn't only about which word is involved — whether the declaration is exported changes it too. Reproduced on four small files, same broken config throughout:

FileMessage
enum Color { ... }Parsing error: The keyword 'enum' is reserved
export enum Color { ... }Parsing error: Unexpected token enum
interface Foo { x: number }Parsing error: The keyword 'interface' is reserved
export interface Foo { x: number }Parsing error: Unexpected token interface

A bare declaration is parsed as an expression statement, which runs the same identifier check described above and hits "is reserved" for any word on Acorn's list — interface included, since modules are always strict mode and interface sits in reservedWords.strict. Acorn's exported form goes through a dedicated parseExport function instead, which only recognises var, const, class, function, let and async function as the start of an export declaration (shouldParseExportStatement); enum and interface aren't on that list, so parseExport falls through to the branch that expects export { ... } specifiers, finds enum or interface where it expected a {, and rejects it immediately as "Unexpected token" — the identifier check never runs. The fix was reproduced against all four rows of the table above, not only the enum row: the same parser swap clears every one of them. A plain type annotation with no enum or interface anywhere fails too, with yet another wording, for the same underlying reason — no TypeScript-aware parser configured:

ts
export function double(x: number): number {
  return x * 2;
}
text
Parsing error: Unexpected token :

The wording differs because : number isn't a reserved word, just a token the plain grammar doesn't expect there, while a bare enum or interface specifically hits Acorn's reserved-word check. The precise wording of a parsing error, not its general shape, is what tells you which exact gap in the grammar caused it — "is reserved" points at Acorn's reserved-word list, "Unexpected token" points at a token the grammar simply has no rule for. The same parser swap — step 2 or 3 above — clears all four wordings in the table, and the type-annotation wording too; the underlying fix is the same for any TypeScript syntax the default parser has no grammar for, whatever the exact wording turns out to be.

ESLint isn't the only tool whose parser throws on code it wasn't built to read, for one of two different reasons. Node's own module loader rejects import statements on an old runtime with a different message, because that runtime predates ES module syntax in .js files — a real gap in time, closed by upgrading Node. This page's enum and interface are not that: they were never part of any ECMAScript edition, so no amount of upgrading Acorn would close the gap, only swapping the parser does. Vite 2's production build fails on node_modules code for a third reason entirely: a define text-replacement step turns a valid identifier into invalid syntax before Rollup's own parser ever sees it, so the code itself, not the parser, is where that one starts. The same symptom, a parser error, can come from a parser that is older than the syntax (the Node case), a parser that was never going to support non-standard syntax at all (this page), or a build step that corrupts otherwise-valid syntax before any parser reads it (the Vite case). Reading the exact wording, as throughout this page, is what tells the three apart.

If the next error is about parserOptions.project, or a version mismatch#

Setting the parser is usually the first of two steps, not the whole journey. The moment a project also wants type-aware rules (anything that needs to know a value's actual type, not just its syntax), parserOptions.project has to point at a tsconfig.json that genuinely includes every linted file — Cypress spec files are one well-documented place this breaks, with a parsing error of its own: see Fix 'ESLint was configured to run on' in Cypress tests for the two ways that file ends up outside every configured TSConfig: an exclude overriding include, or a relative project path that resolves wrong without tsconfigRootDir.

The other common follow-on shows up later, after an upgrade: if @typescript-eslint/parser and @typescript-eslint/eslint-plugin are pinned to an old major version while typescript itself moves forward, the parsing error changes shape again, to DeprecationError: 'originalKeywordKind' has been deprecated — a version-mismatch problem, not a missing-parser one. See Fix 'originalKeywordKind' DeprecationError in TypeScript if that's the message that shows up instead.

Either way, the pattern holds across every case reproduced on this page: ESLint's parsing errors name the exact thing the parser couldn't make sense of, and that exact wording — not the general shape of "a parsing error happened" — is what tells you whether the fix is a missing parser, an incomplete tsconfig.json, or a version that drifted out of sync.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote