How to fix 'jsx' option 'preserve' is not compatible with 'module' option 'es2015' in TypeScript

TypeScriptINTERMEDIATEMEDIUMUpdated Dec 11, 2025· 105 views

This TypeScript configuration error occurs when you set jsx: 'preserve' with module: 'es2015' or similar ES module targets. The preserve mode keeps JSX syntax in output files, which conflicts with ES module syntax requirements. Fix by changing either the jsx or module option to compatible values.

Quick fix

Fixes 'jsx' option 'preserve' is not compatible with 'module' option 'es2015'

// tsconfig.json - BEFORE (causes error)
{
  "compilerOptions": {
    "jsx": "preserve",
    "module": "es2015"
  }
}

// tsconfig.json - AFTER (fixed)
{
  "compilerOptions": {
    "jsx": "react-jsx",  // For React 17+ with automatic JSX runtime
    "module": "es2015"
  }
}
  1. Change jsx mode to 'react-jsx' or 'react'
  2. Change module target to 'commonjs'
  3. Use 'preserve' with 'nodenext' or 'node16' module resolution
  4. Configure separate compilation for JSX files
typescript — error
// tsconfig.json - BEFORE (causes error)
{
"compilerOptions": {
"jsx": "preserve",
"module": "es2015"
}
}
// tsconfig.json - AFTER (fixed)
{
"compilerOptions": {
"jsx": "react-jsx", // For React 17+ with automatic JSX runtime
"module": "es2015"
}
}
'jsx' option 'preserve' is not compatible with 'module' option 'es2015'

What this error means

The error "'jsx' option 'preserve' is not compatible with 'module' option 'es2015'" appears when TypeScript detects an incompatible combination of compiler options in your tsconfig.json file. The jsx: 'preserve' setting tells TypeScript to keep JSX syntax unchanged in the output files, transforming only TypeScript-specific syntax. This is useful when you want another tool (like Babel) to handle JSX transformation. However, when combined with module: 'es2015' (or similar ES module targets like 'es2020', 'esnext'), this creates a conflict because: 1. ES modules expect valid JavaScript syntax 2. JSX syntax (<Component />) is not valid JavaScript 3. TypeScript cannot output JSX syntax in ES module files The compiler prevents this combination to avoid generating invalid JavaScript files that would cause runtime errors. You need to choose either: - A different jsx mode (like 'react-jsx' or 'react') - A different module target (like 'commonjs') - Or use a build pipeline where TypeScript outputs to one format and another tool handles JSX transformation.

Typical symptoms

  • TypeScript compilation fails with the error message
  • Build process stops with configuration error
  • IDE shows error in tsconfig.json file
  • Error appears when running tsc command
  • Project fails to compile after changing jsx or module options
  • CI/CD pipeline fails with TypeScript configuration error

Common causes

  1. Setting jsx: 'preserve' with module: 'es2015', 'es2020', or 'esnext' in tsconfig.json
  2. Copying configuration from a project with different build setup
  3. Migrating from Babel to TypeScript without updating jsx mode
  4. Using preserve mode for React Native or custom JSX transformations
  5. Incorrectly assuming preserve mode works with all module systems
  6. Mixing configuration from different TypeScript versions

How to fix "'jsx' option 'preserve' is not compatible with 'module' option 'es2015'"

1Change jsx mode to 'react-jsx' or 'react'

The simplest fix is to change the jsx option from 'preserve' to a mode that transforms JSX:

json
// tsconfig.json - BEFORE (causes error)
{
  "compilerOptions": {
    "jsx": "preserve",
    "module": "es2015"
  }
}

// tsconfig.json - AFTER (fixed)
{
  "compilerOptions": {
    "jsx": "react-jsx",  // For React 17+ with automatic JSX runtime
    "module": "es2015"
  }
}

Alternative jsx modes:
- "react": Transforms JSX to React.createElement() calls (React 16 and earlier)
- "react-jsx": Uses React 17+ automatic JSX runtime (imports jsx from 'react/jsx-runtime')
- "react-jsxdev": Development version of react-jsx with debugging info

Choose based on your React version:
- React 17+: Use "react-jsx" or "react-jsxdev"
- React 16 or earlier: Use "react"
- Next.js 13+ with app router: Use "preserve" with special configuration

2Change module target to 'commonjs'

If you need to preserve JSX for a specific reason, change the module system instead:

json
// tsconfig.json - BEFORE (causes error)
{
  "compilerOptions": {
    "jsx": "preserve",
    "module": "es2015"
  }
}

// tsconfig.json - AFTER (fixed)
{
  "compilerOptions": {
    "jsx": "preserve",
    "module": "commonjs"  // Changed from es2015
  }
}

CommonJS modules don't have the same syntax restrictions as ES modules, allowing JSX syntax to be preserved. This is useful when:

1. Using Babel for JSX transformation: TypeScript outputs .tsx → .js with JSX preserved, then Babel transforms JSX
2. Custom JSX runtime: You have a non-React JSX implementation
3. React Native projects: Often use preserve mode with custom transformations

Note: If targeting Node.js, CommonJS is still the standard module system.

3Use 'preserve' with 'nodenext' or 'node16' module resolution

For modern Node.js projects using ES modules, you can use preserve mode with specific module settings:

json
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "preserve",
    "module": "nodenext",  // or "node16"
    "moduleResolution": "nodenext"
  }
}

This configuration works because:
- nodenext and node16 are special module modes for Node.js ES modules
- They include additional handling for JSX preservation
- Requires Node.js 16+ with "type": "module" in package.json

json
// package.json
{
  "type": "module",
  "scripts": {
    "build": "tsc"
  }
}

Important: With this setup, you'll need another tool (like Babel) to transform JSX after TypeScript compilation.

4Configure separate compilation for JSX files

If you need different settings for .ts and .tsx files, use multiple tsconfig files:

json
// tsconfig.base.json - Shared settings
{
  "compilerOptions": {
    "strict": true,
    "target": "es2015"
  }
}

// tsconfig.json - For .ts files (no JSX)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "es2015"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["src/**/*.tsx"]
}

// tsconfig.react.json - For .tsx files
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "module": "es2015"
  },
  "include": ["src/**/*.tsx"]
}

Build with both configurations:

bash
# Build .ts files
npx tsc -p tsconfig.json

# Build .tsx files
npx tsc -p tsconfig.react.json

Or use a build script:

json
// package.json
{
  "scripts": {
    "build": "npm run build:ts && npm run build:tsx",
    "build:ts": "tsc -p tsconfig.json",
    "build:tsx": "tsc -p tsconfig.react.json"
  }
}
5Use Babel for JSX transformation with preserve mode

If you must use jsx: 'preserve' with ES modules, set up Babel to handle JSX:

1. Install Babel dependencies:

bash
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript

2. Create .babelrc.json:

json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}

3. Configure tsconfig.json for preservation:

json
{
  "compilerOptions": {
    "jsx": "preserve",
    "module": "es2015",
    "outDir": "./dist-ts"
  }
}

4. Build pipeline:

bash
# Step 1: TypeScript compiles to dist-ts/ with JSX preserved
npx tsc

# Step 2: Babel transforms JSX in dist-ts/ to dist/
npx babel dist-ts --out-dir dist --extensions ".js,.jsx,.ts,.tsx"

5. Or use a build tool like webpack that chains TypeScript and Babel:

javascript
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.tsx?$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-react']
            }
          },
          {
            loader: 'ts-loader',
            options: {
              compilerOptions: {
                jsx: 'preserve'
              }
            }
          }
        ]
      }
    ]
  }
};
6Check TypeScript version and update configuration

Different TypeScript versions have different compatibility rules:

bash
# Check your TypeScript version
npx tsc --version

# Update TypeScript if needed
npm install typescript@latest --save-dev

Compatibility matrix:

| TypeScript | jsx: 'preserve' compatible with |
|------------|----------------------------------|
| 4.1+ | commonjs, amd, system, umd |
| 4.1+ | es2015, es2020, esnext (with limitations) |
| 4.7+ | node16, nodenext (with "type": "module") |

If using an older TypeScript version, consider updating or adjusting expectations.

Also check for conflicting extends configurations:

json
// tsconfig.json
{
  "extends": "@some-package/tsconfig",  // Might override your settings
  "compilerOptions": {
    "jsx": "preserve",
    "module": "es2015"
  }
}

Test without extends to isolate the issue:

bash
# Create minimal config
echo '{"compilerOptions":{"jsx":"preserve","module":"es2015"}}' > test-config.json

# Test compilation
npx tsc -p test-config.json --noEmit

Advanced notes

### Understanding JSX Modes in TypeScript

TypeScript supports several jsx modes:

1. preserve: Keeps JSX syntax unchanged in output
- Output: .tsx → .jsx (with JSX syntax)
- Use case: When another tool (Babel, SWC) handles JSX transformation
- Compatibility: Limited with ES modules

2. react: Transforms JSX to React.createElement()
- Output: <div /> → React.createElement("div", null)
- Use case: React 16 and earlier, or when not using automatic JSX runtime
- Compatibility: Works with all module systems

3. react-jsx: Uses React 17+ automatic JSX runtime
- Output: <div /> → _jsx("div", {})
- Use case: React 17+ with automatic JSX imports
- Requires: Importing from 'react/jsx-runtime'

4. react-jsxdev: Development version of react-jsx
- Adds source location info for debugging
- Larger output files

### Module System Compatibility

The compatibility issue arises because:

- ES modules (es2015, es2020, esnext): Must contain valid JavaScript syntax
- JSX syntax: Is not valid JavaScript (needs transformation)
- TypeScript's dilemma: Cannot output invalid syntax in ES module files

Why preserve + commonjs works:
CommonJS modules (require/module.exports) can contain any syntax since they're evaluated differently. The JSX syntax will cause runtime errors unless transformed, but TypeScript allows the compilation.

### Modern Alternatives to preserve Mode

Instead of jsx: 'preserve', consider:

1. Using SWC: Faster than Babel, handles TypeScript and JSX

bash
npm install @swc/core @swc/cli
   npx swc src --out-dir dist

2. esbuild: Extremely fast, supports JSX preservation

bash
npm install esbuild
   npx esbuild src/index.tsx --bundle --outfile=dist/bundle.js --jsx=preserve

3. Vite: Uses esbuild under the hood, great for development

bash
npm create vite@latest my-app -- --template react-ts

### Next.js Specific Configuration

Next.js 13+ with app router has special requirements:

json
// tsconfig.json for Next.js app router
{
  "compilerOptions": {
    "jsx": "preserve",  // Next.js handles JSX transformation
    "module": "esnext",
    "moduleResolution": "bundler",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowImportingTsExtensions": true
  }
}

Next.js uses its own compiler (SWC) which handles the JSX transformation, so preserve mode is correct despite using ES modules.

### Debugging Configuration Issues

Use TypeScript's diagnostic output:

bash
# Show all compiler options being used
npx tsc --showConfig

# Dry run to see errors without emitting files
npx tsc --noEmit

# Verbose output for debugging
npx tsc --extendedDiagnostics

Check for configuration file precedence:
1. Command line options (highest priority)
2. tsconfig.json in current directory
3. tsconfig.json in parent directories
4. Default compiler options

Frequently asked questions

What does "'jsx' option 'preserve' is not compatible with 'module' option 'es2015" mean?
json file. The jsx: 'preserve' setting tells TypeScript to keep JSX syntax unchanged in the output files, transforming only TypeScript-specific syntax. This is useful when you want another tool (like Babel) to handle JSX transformation.
How do I fix "'jsx' option 'preserve' is not compatible with 'module' option 'es2015"?
This TypeScript configuration error occurs when you set jsx: 'preserve' with module: 'es2015' or similar ES module targets. The preserve mode keeps JSX syntax in output files, which conflicts with ES module syntax requirements. Fix by changing either the jsx or module option to compatible values.
What causes "'jsx' option 'preserve' is not compatible with 'module' option 'es2015"?
Setting jsx: 'preserve' with module: 'es2015', 'es2020', or 'esnext' in tsconfig.json Copying configuration from a project with different build setup Migrating from Babel to TypeScript without updating jsx mode
Is the "'jsx' option 'preserve' is not compatible with 'module' option 'es2015" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading