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.
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.
The simplest fix is to change the jsx option from 'preserve' to a mode that transforms JSX:
// 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
If you need to preserve JSX for a specific reason, change the module system instead:
// 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.
For modern Node.js projects using ES modules, you can use preserve mode with specific module settings:
// 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
// package.json
{
"type": "module",
"scripts": {
"build": "tsc"
}
}Important: With this setup, you'll need another tool (like Babel) to transform JSX after TypeScript compilation.
If you need different settings for .ts and .tsx files, use multiple tsconfig files:
// 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:
# Build .ts files
npx tsc -p tsconfig.json
# Build .tsx files
npx tsc -p tsconfig.react.jsonOr use a build script:
// 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"
}
}If you must use jsx: 'preserve' with ES modules, set up Babel to handle JSX:
1. Install Babel dependencies:
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript2. Create .babelrc.json:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
]
}3. Configure tsconfig.json for preservation:
{
"compilerOptions": {
"jsx": "preserve",
"module": "es2015",
"outDir": "./dist-ts"
}
}4. Build pipeline:
# 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:
// 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'
}
}
}
]
}
]
}
};Different TypeScript versions have different compatibility rules:
# Check your TypeScript version
npx tsc --version
# Update TypeScript if needed
npm install typescript@latest --save-devCompatibility 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:
// tsconfig.json
{
"extends": "@some-package/tsconfig", // Might override your settings
"compilerOptions": {
"jsx": "preserve",
"module": "es2015"
}
}Test without extends to isolate the issue:
# Create minimal config
echo '{"compilerOptions":{"jsx":"preserve","module":"es2015"}}' > test-config.json
# Test compilation
npx tsc -p test-config.json --noEmit### 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
npm install @swc/core @swc/cli
npx swc src --out-dir dist2. esbuild: Extremely fast, supports JSX preservation
npm install esbuild
npx esbuild src/index.tsx --bundle --outfile=dist/bundle.js --jsx=preserve3. Vite: Uses esbuild under the hood, great for development
npm create vite@latest my-app -- --template react-ts### Next.js Specific Configuration
Next.js 13+ with app router has special requirements:
// 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:
# 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 --extendedDiagnosticsCheck 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
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript