ESLint 24 🛡️ Custom Parsers — Swapping Espree for Babel or TypeScript
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
| Package | Use case | Required options |
|---|---|---|
| Espree (default) | Standard JavaScript | None |
@babel/eslint-parser | Babel syntax | requireConfigFile or a Babel config |
@typescript-eslint/parser | TypeScript | sourceType: 'module' for ESM |
vue-eslint-parser | Vue | parserOptions.parser for the script block |
The TypeScript parser modes
| Mode | Option | Requires tsconfig.json |
|---|---|---|
| Syntax-only | None | No |
| Type-aware | projectService: true | Yes |
The common errors
| Error | Cause | Fix |
|---|---|---|
Parsing error: Unexpected token : | Espree parsing TypeScript | Use @typescript-eslint/parser |
No Babel config file detected | @babel/eslint-parser without config | Set requireConfigFile: false or add a config |
The keyword 'import' is reserved | sourceType not set | Set sourceType: 'module' |
| The parser is not applied | No files pattern | Add 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The parser is not applied | No files pattern or the pattern does not match | Check the glob |
| Type-aware rules do not work | projectService not set | Set projectService: true and tsconfigRootDir |
| The Babel parser throws | No Babel config | Set requireConfigFile: false |
| The Vue script block is not parsed | parserOptions.parser not set | Set 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
| Item | Value |
|---|---|
| Default parser | Espree |
| Babel parser | @babel/eslint-parser |
| TypeScript parser | @typescript-eslint/parser |
| Vue parser | vue-eslint-parser |
| Parser property | languageOptions.parser (object, not string) |
| Babel option | requireConfigFile: false without a config |
| TypeScript option | sourceType: 'module' for ESM |
| Type-aware option | projectService: 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-basedparser: "@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. ThebabelOptionsobject 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 onimportandexportstatements . - The type-aware mode requires
projectService: trueandtsconfigRootDir. TheprojectServiceoption is preferred over the olderprojectoption because it handles thetsconfig.jsondiscovery automatically. ThetsconfigRootDiranchors the resolution when the config does not run from the repository root . - The parser is scoped by the
filespattern. A config object without afilespattern 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.
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!