
Listen to this story
Let's be honest - we've all been there. You're working on a TypeScript project, everything's going great, and then you open that tsconfig.json
file. Suddenly you're staring at a labyrinth of cryptic options, and you just copy-paste something from Stack Overflow and pray it works.
Sound familiar?
Here's the thing: tsconfig doesn't have to be intimidating. Behind all those options, there are really just a few essential settings you need to understand. The rest? They're there when you need them, but you can safely ignore them until that day comes.
This isn't just another tsconfig guide. This is the guide I wish I had when I started with TypeScript – clear, practical, and bs-free.
These five properties form the foundation of any solid TypeScript configuration:
This tells TypeScript what "version" of JavaScript to create when it converts your TypeScript code.
"target": "ES2022"
Why it matters: Modern JavaScript (newer versions) has cool features that make your code cleaner and faster, but older browsers might not understand them.
When to change it:
"ES2022"
or newer for modern environments (Node.js, modern browsers)"ES6"
if you need to support slightly older browsers"ES5"
only if you need to support really old browsers (Internet Explorer)
Problem it solves: Ensures your code runs in your target environment without needing complex build setups.
This tells TypeScript how to handle import
and export
statements in your code.
"module": "NodeNext"
Why it matters: Different environments handle modules differently, and the wrong setting can break your app entirely.
When to change it:
"NodeNext"
for modern Node projects"ESNext"
"ES2015"
or "ESNext"
Problem it solves: Makes sure your imports and exports work correctly in your target environment.
This tells TypeScript how to locate files when you write import { something } from 'somewhere'
.
"moduleResolution": "NodeNext"
Why it matters: If TypeScript can't find your imports, you'll get frustrating errors.
When to change it:
"NodeNext"
"Bundler"
"Node"
(classic Node.js resolution)
Problem it solves: Prevents "Cannot find module" errors and ensures imports resolve correctly.
This turns on TypeScript's strict checking, which is like having a very picky code reviewer built into your editor.
"strict": true
Why it matters: Catches many common bugs before your code even runs.
When to change it:
true
false
and gradually move to true
Problem it solves: Prevents entire categories of bugs like null reference errors, undefined variables, and implicit type conversions.
Tells TypeScript which files to check and compile.
"include": ["src/**/*"]
Why it matters: Helps TypeScript focus only on the files that matter, making compilation faster.
When to change it:
Problem it solves: Speeds up compilation and prevents TypeScript from processing files you don't want it to.
When building a Node.js application, these settings are particularly important:
Tells TypeScript where to put the JavaScript files it creates.
"outDir": "dist"
Why it matters: Keeps your source code separate from compiled output, making your project cleaner.
Problem it solves: Prevents compiled code from cluttering your source directories, making it easier to deploy only what's needed.
Makes it easier to use packages that use the older CommonJS format with modern ESM imports.
"esModuleInterop": true
Why it matters: Node.js has two module systems (CommonJS and ESM), and this helps them work together.
Problem it solves: Prevents bizarre import errors when mixing package types, especially with older libraries.
Lets you import JSON files as if they were TypeScript/JavaScript modules.
"resolveJsonModule": true
Why it matters: Makes working with configuration and data files much simpler.
Problem it solves: Eliminates the need for workarounds to import JSON data in your Node.js applications.
React projects with bundlers (like webpack/Vite/esbuild/Next.js) have different needs:
Tells TypeScript how to process React's special JSX syntax.
"jsx": "react-jsx"
Why it matters: React components use JSX, which needs to be transformed into regular JavaScript.
Problem it solves: Ensures your React components compile correctly and integrate with your bundler.
Tells TypeScript to only check your code for errors but not generate any output files.
"noEmit": true
Why it matters: In React projects, your bundler (webpack/Vite) handles the code transformation, not TypeScript.
Problem it solves: Prevents duplicate processing and avoids conflicts between TypeScript and your bundler.
Makes TypeScript warn you if you use features that only work with the TypeScript compiler but would break with simpler transpilers.
"isolatedModules": true
When you need it: When using bundlers like webpack, Vite, or transpilers like Babel or swc that process files individually.
Problem it solves: Prevents mysterious build errors when your code moves from development to production bundling by ensuring your TypeScript code is compatible with any transpilation setup.
One crucial thing to understand: TypeScript doesn't work alone in modern JavaScript development.
In a Node.js application, TypeScript is both the type-checker and the compiler. It directly transforms your TypeScript code into JavaScript that Node.js can run. This means your tsconfig settings directly affect the output code.
[TypeScript Files] → [TypeScript Compiler] → [JavaScript Files] → [Node.js Runtime]
In a React application, the workflow is different:
[TypeScript Files] → [TypeScript Type-Checking] → [Bundler (webpack/Vite/etc.)] → [JavaScript Bundle] → [Browser]
In this scenario, TypeScript primarily serves as a type-checker, and the bundler handles the actual transformation to JavaScript. This is why many React project configs include "noEmit": true
- TypeScript isn't producing the final JavaScript output.
Here's a key insight many developers miss: in bundler-based workflows, many of your tsconfig settings become suggestions rather than requirements. The bundler may:
This is why your React/Vite configuration has different settings than your Node.js configuration. It's not just a different target environment; it's a fundamentally different build process.
For React applications, your bundler config (vite.config.js, webpack.config.js, etc.) typically has the final say on:
For example esbuild ignores almost all tsconfig settings, with the exception of a few.
This is why the React tsconfig recipe above focuses on type-checking settings and leaves many transformation details to the bundler.
These properties aren't essential for every project, but they solve specific problems you might encounter:
Tells TypeScript to not thoroughly check the types in library files (node_modules).
"skipLibCheck": true
When you need it: When your builds are taking forever because of large dependencies.
Problem it solves: Dramatically speeds up TypeScript compilation by trusting that your dependencies have correct types.
Lets you create shortcuts for import paths so you can write cleaner imports.
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
When you need it: When you're tired of writing ../../../../
in your import statements.
Problem it solves: Makes imports cleaner and more maintainable, especially in larger projects.
Generates type definition files so others can use your code with TypeScript.
"declaration": true,
"declarationMap": true
When you need it: When you're building a library for others to use.
Problem it solves: Makes your library TypeScript-friendly for consumers.
Tells TypeScript which built-in objects and APIs your code can use.
"lib": ["DOM", "DOM.Iterable", "ESNext"]
When you need it: When you want to control exactly which browser/JavaScript features your code can access.
Problem it solves: Prevents your code from using features that aren't available in your target environment.
Instead of struggling to piece together the right configuration, here are ready-to-use configurations for common project types. Consider these your starting templates, not the final word.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"isolatedModules": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Here’s more inspirations with tsconfig setups tsconfig/bases.
Let's look at some common errors you might encounter and how to fix them with tsconfig:
TS2307: Cannot find module 'some-package' or its corresponding type declarations.
What's happening: TypeScript can't find the module you're trying to import.
Potential fixes:
moduleResolution
is correct for your environmentesModuleInterop
is set to true
npm install @types/package-name --save-dev
include
paths
The detective approach: These errors often have multiple potential causes. Instead of random tweaking, follow this systematic approach:
TS2307: Cannot find module '@components/Button' or its corresponding type declarations.
What's happening: Your path aliases aren't being resolved correctly.
Potential fixes:
baseUrl
and paths
correctlyinclude
patterns
Common gotcha: Path aliases need configuration in both tsconfig.json AND your bundler (webpack/Vite). Forgetting either side will cause mysterious errors where things compile but break at runtime.
TS2339: Property 'someProperty' does not exist on type...
What's happening: TypeScript is finding type errors in your node_modules.
Potential fixes:
skipLibCheck
to true
to ignore errors in declaration filesHere's the truth: most TypeScript projects only need 5-7 key tsconfig options to work correctly. The rest are there for specialized cases.
Instead of memorizing all the options or blindly copying configurations, focus on understanding these core properties and what problems they solve. Over time, you'll develop an intuition for which properties you need to adjust when you encounter specific issues.
Remember:
With this approach, you'll never be intimidated by a tsconfig file again. You'll approach each new TypeScript project with confidence, knowing exactly which options matter and why.
The next time someone on your team says "I don't understand what's happening with TypeScript," send them this guide. We all deserve to work with TypeScript without the configuration anxiety.