| | |

ESLint 24 🛡️ Custom Parsers — Swapping Espree for Babel or TypeScript

⏱️ Estimated read: 11 min | |

ESLint’s default parser is Espree, and for standard JavaScript it is sufficient. When the source code uses syntax that Espree does not understand—TypeScript’s type annotations, Babel’s experimental proposals, or JSX in non-standard file extensions—the parser must be replaced. The languageOptions.parser property in flat config is the mechanism. It accepts a parser object, not a string, and the parser object must conform to the ESLint parser interface: it exports a parse or parseForESLint function that returns an ESTree-compatible AST . The two parsers that cover the common cases are @babel/eslint-parser for Babel-transformed syntax and @typescript-eslint/parser for TypeScript.

The swap is not a one-line change in every project. Each parser has its own options, its own requirements, and its own failure modes. The Babel parser requires a Babel configuration file unless requireConfigFile: false is set, and it transforms the Babel AST into the ESTree format that ESLint’s rules expect . The TypeScript parser requires sourceType: 'module' to be set explicitly for ESM code, and it has a separate mode for type-aware linting that requires a tsconfig.json . The parser swap is the first step; the options are the second.

Key point: The parser is an object imported from the package, assigned to languageOptions.parser in a config object scoped by files. The Babel parser needs a Babel config or requireConfigFile: false. The TypeScript parser needs sourceType: 'module' for ESM and a tsconfig.json for type-aware rules. The parser does not provide rules; it only provides the AST.


Why the parser must be swapped

The syntax gap. Espree parses the latest finalized ECMAScript standard. It does not parse TypeScript type annotations, Babel’s decorator proposals, or the pipeline operator. A file with const x: number = 1 fails to parse, and the error is a parsing error, not a rule violation . The custom parser is the mechanism that produces an AST for the syntax that Espree does not understand.

The ESTree compatibility requirement. ESLint’s rules are written against the ESTree specification, which is the AST format that Espree produces. Babel’s parser produces a different AST format. The @babel/eslint-parser package wraps the Babel parser and converts its AST into the ESTree format, preserving the location information so that the rules can report the correct line and column . The TypeScript parser does the same for TypeScript’s AST . A parser that does not produce an ESTree-compatible AST will cause the rules to fail or report incorrect locations.

The file-pattern scoping. A project can use different parsers for different files. A JavaScript file uses Espree, a TypeScript file uses @typescript-eslint/parser, and a Vue file uses vue-eslint-parser. The flat config’s files pattern scopes the parser to the file types it handles . A config object without a files pattern applies the parser to every file, which is rarely the intent.

The trade-off. The parser swap adds a dependency and a configuration object. The Babel parser adds a Babel configuration requirement, and the TypeScript parser adds a tsconfig.json requirement for the type-aware mode. The parser does not add rules; the rules come from the plugin or the core config. The swap is necessary only when the syntax requires it, and the configuration is the cost of the syntax.


a. Swapping to @babel/eslint-parser

The Babel parser is for code that is transformed by Babel, not for TypeScript. It parses the experimental syntax that Babel supports, and it converts the result to ESTree. The parser requires @babel/core as a peer dependency, and it loads the project’s Babel configuration by default.

// eslint.config.mjs — the Babel parser
import babelParser from "@babel/eslint-parser";

export default [
  {
    files: ["**/*.js", "**/*.mjs"],
    languageOptions: {
      // The parser is the imported object, not a string.
      parser: babelParser,
      parserOptions: {
        // The requireConfigFile option allows the parser to run
        // on files that do not have a Babel configuration.
        // Without it, the parser throws if no config is found.
        requireConfigFile: false,
        // The babelOptions are passed to the Babel parser.
        // The presets enable the syntax that the project uses.
        babelOptions: {
          presets: ["@babel/preset-env"],
        },
      },
    },
  },
];

The requireConfigFile: false option is the escape hatch for a project that does not have a Babel config file. Without it, the parser looks for a Babel configuration and throws an error if it does not find one . The babelOptions object passes the configuration directly to the parser, which is useful when the project does not have a .babelrc or babel.config.js file.

// eslint.config.mjs — the Babel parser with the React preset
import babelParser from "@babel/eslint-parser";

export default [
  {
    files: ["**/*.jsx"],
    languageOptions: {
      parser: babelParser,
      parserOptions: {
        requireConfigFile: false,
        babelOptions: {
          presets: ["@babel/preset-react"],
        },
      },
    },
  },
];

The @babel/preset-react is the preset for JSX syntax. The parser uses the preset to parse the JSX, and the ESTree conversion preserves the JSX nodes so that the React rules can analyze them.


b. Swapping to @typescript-eslint/parser

The TypeScript parser is for .ts and .tsx files. It parses the TypeScript syntax and converts the AST to ESTree. The parser has two modes: the syntax-only mode, which does not require a tsconfig.json, and the type-aware mode, which does and exposes the type information to the rules.

// eslint.config.mjs — the TypeScript parser, syntax-only mode
import tsParser from "@typescript-eslint/parser";

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        // The sourceType must be set explicitly for ESM code.
        // The parser falls back to "script" when ESLint
        // does not provide the option.
        sourceType: "module",
        ecmaVersion: "latest",
      },
    },
  },
];

The sourceType: 'module' option is required for ESM code. The parser’s documentation states that it falls back to 'script' when ESLint does not provide the option, which produces parsing errors on import and export statements . The ecmaVersion: 'latest' option enables the latest syntax that the parser supports.

// eslint.config.mjs — the TypeScript parser, type-aware mode
import tsParser from "@typescript-eslint/parser";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        sourceType: "module",
        // The projectService option enables type-aware linting.
        // It is the preferred choice over the project option.
        projectService: true,
        // The tsconfigRootDir anchors the tsconfig resolution.
        tsconfigRootDir: __dirname,
      },
    },
  },
];

The projectService: true option is the current recommended way to enable type-aware linting. It is preferred over the older project option because it handles the tsconfig.json discovery automatically . The tsconfigRootDir is required when the ESLint config is not guaranteed to run from the repository root.


Complete Example Session

// ============================================
// PART 1: THE DEFAULT PARSER (ESPREE)
// ============================================

// A standard JavaScript file is parsed by Espree.
// No parser configuration is needed.
import js from "@eslint/js";

export default [
  js.configs.recommended,
  {
    files: ["**/*.js"],
    // No parser property; Espree is the default.
  },
];

// ============================================
// PART 2: THE BABEL PARSER
// ============================================

// A file with experimental syntax.
import babelParser from "@babel/eslint-parser";

export default [
  {
    files: ["**/*.jsx"],
    languageOptions: {
      parser: babelParser,
      parserOptions: {
        requireConfigFile: false,
        babelOptions: {
          presets: ["@babel/preset-react"],
        },
      },
    },
  },
];

// ============================================
// PART 3: THE TYPESCRIPT PARSER
// ============================================

// A TypeScript file.
import tsParser from "@typescript-eslint/parser";

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        sourceType: "module",
        ecmaVersion: "latest",
      },
    },
  },
];

// ============================================
// PART 4: THE TYPESCRIPT TYPE-AWARE PARSER
// ============================================

// A TypeScript file with type-aware rules.
import tsParser from "@typescript-eslint/parser";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        sourceType: "module",
        projectService: true,
        tsconfigRootDir: __dirname,
      },
    },
  },
];

// ============================================
// PART 5: THE PARSER OPTIONS
// ============================================

// The parserOptions are parser-specific.
// The Babel parser has requireConfigFile and babelOptions.
// The TypeScript parser has projectService and tsconfigRootDir.
// The options are passed directly to the parser.

// ============================================
// PART 6: THE FILE PATTERN SCOPING
// ============================================

// The parser is scoped by the files pattern.
// A .js file uses Espree; a .ts file uses the TypeScript parser.
export default [
  {
    files: ["**/*.js"],
    // Espree
  },
  {
    files: ["**/*.ts"],
    languageOptions: {
      parser: tsParser,
    },
  },
];

// ============================================
// PART 7: THE VUE PARSER
// ============================================

// A Vue file uses vue-eslint-parser, which delegates
// the script block to the TypeScript parser.
import vueParser from "vue-eslint-parser";
import tsParser from "@typescript-eslint/parser";

export default [
  {
    files: ["**/*.vue"],
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        parser: tsParser,
      },
    },
  },
];

// ============================================
// PART 8: THE PARSER INTERFACE
// ============================================

// A custom parser must export a parse or parseForESLint function.
// It must return an ESTree-compatible AST.
// The @babel/eslint-parser and @typescript-eslint/parser
// both conform to the interface.

// ============================================
// PART 9: THE VERIFICATION
// ============================================

// npx eslint . --print-config src/index.ts
//
// The command prints the resolved config for the file.
// It shows the parser that applies to the file.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The parser package is installed.
// [ ] The parser is imported, not referenced by string.
// [ ] The parser is assigned to languageOptions.parser.
// [ ] The files pattern scopes the parser.
// [ ] The parserOptions are set for the parser.
// [ ] The sourceType is set for the TypeScript parser.
// [ ] The requireConfigFile is set for the Babel parser.
// [ ] The config is verified with --print-config.

The ten parts covered the default parser, the Babel parser, the TypeScript parser, the type-aware mode, the options, the file scoping, the Vue parser, the parser interface, the verification, and the checklist. The through-line is that the parser is an object, and the options are parser-specific.


Quick Reference

The parser packages

PackageUse caseRequired options
Espree (default)Standard JavaScriptNone
@babel/eslint-parserBabel syntaxrequireConfigFile or a Babel config
@typescript-eslint/parserTypeScriptsourceType: 'module' for ESM
vue-eslint-parserVueparserOptions.parser for the script block

The TypeScript parser modes

ModeOptionRequires tsconfig.json
Syntax-onlyNoneNo
Type-awareprojectService: trueYes

The common errors

ErrorCauseFix
Parsing error: Unexpected token :Espree parsing TypeScriptUse @typescript-eslint/parser
No Babel config file detected@babel/eslint-parser without configSet requireConfigFile: false or add a config
The keyword 'import' is reservedsourceType not setSet sourceType: 'module'
The parser is not appliedNo files patternAdd the files pattern to the config object

Best Practices

✅ Do This:

// Import the parser
import tsParser from "@typescript-eslint/parser";                  // ✅

// Assign it to languageOptions.parser
languageOptions: { parser: tsParser }                              // ✅

// Scope it with a files pattern
files: ["**/*.ts", "**/*.tsx"]                                     // ✅

// Set sourceType for ESM
parserOptions: { sourceType: "module" }                            // ✅

❌ Don’t Do This:

// Use the string name
languageOptions: { parser: "@typescript-eslint/parser" }           // ❌

// Omit the files pattern
{ languageOptions: { parser: tsParser } } // applies to all files  // ❌

// Forget requireConfigFile for Babel
parser: babelParser // throws without a Babel config               // ❌

Common Pitfalls

PitfallWhy It HappensFix
The parser is not appliedNo files pattern or the pattern does not matchCheck the glob
Type-aware rules do not workprojectService not setSet projectService: true and tsconfigRootDir
The Babel parser throwsNo Babel configSet requireConfigFile: false
The Vue script block is not parsedparserOptions.parser not setSet parserOptions: { parser: tsParser }

Real-World Examples

1. Babel parser

import babelParser from "@babel/eslint-parser"; languageOptions: { parser: babelParser }

2. Babel with requireConfigFile

parserOptions: { requireConfigFile: false, babelOptions: { presets: ["@babel/preset-env"] } }

3. TypeScript parser

import tsParser from "@typescript-eslint/parser"; languageOptions: { parser: tsParser }

4. TypeScript sourceType

parserOptions: { sourceType: "module" }

5. Type-aware mode

parserOptions: { projectService: true, tsconfigRootDir: __dirname }

6. Vue parser

import vueParser from "vue-eslint-parser"; languageOptions: { parser: vueParser }

7. Vue with TypeScript

parserOptions: { parser: tsParser }

8. File scoping

files: ["**/*.ts", "**/*.tsx"]

9. The ecmaVersion

parserOptions: { ecmaVersion: "latest" }

10. The verification

npx eslint . --print-config src/index.ts

Visual

The Parser Swap

┌──────────────────────────────────────────────────────────────┐
│  SOURCE → PARSER → ESTREE AST → RULES                         │
│                                                              │
│  Standard JS:                                                │
│  ┌─────────────────────┐                                    │
│  │ Espree (default)    │  → ESTree AST                      │
│  └─────────────────────┘                                    │
│                                                              │
│  Babel syntax:                                               │
│  ┌─────────────────────┐     ┌─────────────────────┐        │
│  │ @babel/eslint-parser│ ──▶ │ Babel AST → ESTree  │        │
│  └─────────────────────┘     └─────────────────────┘        │
│                                                              │
│  TypeScript:                                                 │
│  ┌─────────────────────┐     ┌─────────────────────┐        │
│  │ @typescript-eslint/ │ ──▶ │ TS AST → ESTree     │        │
│  │ parser              │     │                     │        │
│  └─────────────────────┘     └─────────────────────┘        │
│                                                              │
│  The parser produces the AST. The rules consume it.         │
└──────────────────────────────────────────────────────────────┘

The TypeScript Parser Modes

┌──────────────────────────────────────────────────────────────┐
│  SYNTAX-ONLY VS TYPE-AWARE                                    │
│                                                              │
│  Syntax-only                    Type-aware                   │
│  ┌─────────────────────┐       ┌─────────────────────┐      │
│  │ parser: tsParser    │       │ parser: tsParser    │      │
│  │ sourceType: 'module'│       │ sourceType: 'module'│      │
│  │                     │       │ projectService: true│      │
│  │ No tsconfig needed  │       │ tsconfigRootDir     │      │
│  │                     │       │                     │      │
│  │ Parses the syntax   │       │ Parses and exposes  │      │
│  │                     │       │ type information    │      │
│  └─────────────────────┘       └─────────────────────┘      │
└──────────────────────────────────────────────────────────────┘

The File Scoping

┌──────────────────────────────────────────────────────────────┐
│  DIFFERENT PARSERS FOR DIFFERENT FILES                        │
│                                                              │
│  { files: ["**/*.js"], languageOptions: { /* Espree */ } }   │
│  { files: ["**/*.ts"], languageOptions: { parser: tsParser } }│
│  { files: ["**/*.vue"], languageOptions: { parser: vueParser }}│
│                                                              │
│  The files pattern selects the parser for the file type.    │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Default parserEspree
Babel parser@babel/eslint-parser
TypeScript parser@typescript-eslint/parser
Vue parservue-eslint-parser
Parser propertylanguageOptions.parser (object, not string)
Babel optionrequireConfigFile: false without a config
TypeScript optionsourceType: 'module' for ESM
Type-aware optionprojectService: true + tsconfigRootDir

Key takeaways:

  • The parser is an object, not a string. In flat config, the parser is imported from the package and assigned to languageOptions.parser. The string-based parser: "@typescript-eslint/parser" is the legacy syntax and does not work in flat config .
  • The Babel parser requires a Babel config or requireConfigFile: false. Without the option, the parser throws an error when no Babel configuration is found. The babelOptions object passes the configuration directly to the parser .
  • The TypeScript parser requires sourceType: 'module' for ESM. The parser falls back to 'script' when ESLint does not provide the option, which produces parsing errors on import and export statements .
  • The type-aware mode requires projectService: true and tsconfigRootDir. The projectService option is preferred over the older project option because it handles the tsconfig.json discovery automatically. The tsconfigRootDir anchors the resolution when the config does not run from the repository root .
  • The parser is scoped by the files pattern. A config object without a files pattern applies the parser to every file, which is rarely the intent. The pattern selects the parser for the file type .

Remember: The parser swap is the mechanism that lets ESLint understand syntax that Espree does not. The Babel parser handles experimental syntax, and the TypeScript parser handles TypeScript. The parser is an object, and the options are parser-specific. The Babel parser needs a config or requireConfigFile: false, and the TypeScript parser needs sourceType: 'module'. The type-aware mode needs projectService and a tsconfig.json. The parser does not add rules; it adds the AST. The swap is necessary only when the syntax requires it, and the configuration is the cost of the syntax.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!