TL;DR: tsconfig.json is the configuration file that tells TypeScript how to read, check, and compile your code. It lives in the root of your project and controls things like how strict the type checking is, what JavaScript version to output, and where your source files live. You don't need to memorize it — but you do need to know what to look for when your build breaks.
Why AI Coders Need to Know This
If you're building with AI tools and you've ever touched a TypeScript project, you've seen a tsconfig.json. It shows up automatically when Claude Code, Cursor, or ChatGPT scaffolds almost any modern web app — Next.js, Express, React, NestJS. It's just always there.
Most of the time, it works fine and you ignore it. But then one day you copy a project setup from Stack Overflow, or you switch from Next.js to a plain Node.js backend, or you ask AI to add path aliases so your imports look cleaner — and suddenly you're getting type errors that didn't exist before, or cannot find module errors that make no sense.
That's the moment you need to understand what tsconfig.json is actually doing. Not because you're going to become a TypeScript expert — but because knowing the key options means you can tell your AI exactly what's wrong and get a fix in one prompt instead of ten rounds of debugging in the dark.
Here's when tsconfig.json specifically comes up in a vibe coding workflow:
- Starting a new TypeScript project — AI generates one automatically as part of the scaffold
- Adding path aliases — you want
@/components/Buttoninstead of../../../components/Button - Switching frameworks — moving between Next.js, plain React, and Node.js requires different configs
- Turning strict mode on or off — sometimes AI generates configs that are too strict for an existing codebase
- Getting mysterious build errors — the config is often the culprit when errors seem unrelated to your actual code
Real Scenario
You're starting a new backend API. You open Claude Code and type:
Prompt I Would Type
Create a new Node.js + Express REST API using TypeScript. I want:
- A src/ folder for all my code
- A dist/ folder where the compiled JavaScript goes
- Path aliases so I can import from @/routes, @/middleware, @/models
- Strict TypeScript so I catch bugs early
- A working tsconfig.json set up correctly for Node.js (not Next.js)
Show me the tsconfig.json and explain what each option does.
Claude generates a project scaffold. Among the files is a tsconfig.json in the root directory. Let's look at what it produced and what each line actually means.
What AI Generated
Here's a realistic tsconfig.json for a Node.js + Express project, with plain-English explanations for each option:
{
"compilerOptions": {
// --- OUTPUT SETTINGS ---
// "target": What version of JavaScript to compile your TypeScript into.
// "ES2022" means modern JavaScript. Node.js 18+ handles this fine.
// For older environments or browsers, you'd use "ES5" or "ES2015".
"target": "ES2022",
// "module": How imports/exports are handled in the compiled output.
// "CommonJS" is the Node.js standard (require/module.exports style).
// For Next.js or browser apps, you'd use "ESNext" instead.
"module": "CommonJS",
// "moduleResolution": How TypeScript finds files when you write an import.
// "Node16" matches how Node.js actually resolves modules.
// For Next.js or Vite, "Bundler" is often the right choice.
"moduleResolution": "Node16",
// "outDir": Where to put the compiled JavaScript files.
// TypeScript reads from src/, compiles, and writes the .js output to dist/.
"outDir": "./dist",
// "rootDir": Where your TypeScript source files live.
// TypeScript won't look outside this folder for source files.
"rootDir": "./src",
// --- TYPE CHECKING STRICTNESS ---
// "strict": Turns on ALL of TypeScript's strict checks at once.
// This catches the most bugs but can produce a lot of errors in existing code.
// For new projects: set to true. For adding TypeScript to old JS code: start with false.
"strict": true,
// "noUncheckedIndexedAccess": When you do array[0] or object["key"],
// TypeScript assumes the result might be undefined.
// Forces you to check before using it. More annoying, more correct.
"noUncheckedIndexedAccess": true,
// --- INTEROP SETTINGS ---
// "esModuleInterop": Lets you import CommonJS modules using the cleaner ES import syntax.
// Example: import express from 'express' instead of const express = require('express')
// Almost always set to true. You almost never need to change this.
"esModuleInterop": true,
// "skipLibCheck": Skips type-checking the .d.ts files in node_modules.
// Speeds up compilation and avoids errors caused by type bugs in third-party libraries.
// Set to true in almost every project. Safe to leave on.
"skipLibCheck": true,
// "resolveJsonModule": Lets you import .json files directly in TypeScript.
// Example: import config from './config.json'
"resolveJsonModule": true,
// "forceConsistentCasingInFileNames": Prevents import casing bugs.
// On Mac and Windows, the filesystem isn't case-sensitive, but Linux (your server) is.
// This catches: import Button from './button' when the file is Button.tsx
"forceConsistentCasingInFileNames": true,
// --- PATH ALIASES ---
// "baseUrl": The root path that path aliases are resolved from.
// Setting it to "." means the project root.
"baseUrl": ".",
// "paths": Shortcut aliases for your import paths.
// "@/*" maps to "src/*" — so import foo from "@/routes/users"
// resolves to src/routes/users.ts
"paths": {
"@/*": ["src/*"]
},
// "declaration": Generates .d.ts type definition files alongside your .js output.
// Only needed if you're publishing a library for others to import.
// For an internal API, you can set this to false.
"declaration": true,
// "sourceMap": Generates .map files that connect compiled JS back to your TypeScript source.
// Makes debugging much easier — errors in logs point to the TypeScript line, not the compiled JS.
"sourceMap": true
},
// "include": Which files TypeScript should process.
// Only compile files inside the src/ folder.
"include": ["src/**/*"],
// "exclude": Which files to skip.
// node_modules is always excluded — you don't want to compile your dependencies.
// dist is excluded so TypeScript doesn't try to re-compile its own output.
"exclude": ["node_modules", "dist"]
}
That's a lot of options — but once you understand the categories (output, strictness, interop, paths), it's not overwhelming. The ones you'll actually touch most are: strict, target, module, outDir, and paths.
Framework shortcut: If you're using Next.js, you almost never touch tsconfig.json manually. Next.js manages it for you and auto-updates it when it adds new features. The config it generates uses "extends": "next/core-web-vitals" — which means it's inheriting a base config from the Next.js package and only overriding a few things. That's the extends field in action.
What AI Gets Wrong About tsconfig
AI generates a valid tsconfig most of the time — but "valid" doesn't always mean "right for your specific project." Here are the most common mistakes:
1. Using a Next.js config for a plain Node.js project (and vice versa)
This is the most common mismatch. Next.js and plain Node.js need different module and moduleResolution settings. If you copy a Next.js tsconfig.json into a Node.js Express project, you'll get module resolution errors that look completely unrelated to what you were doing.
| Setting | Next.js | Node.js / Express | React (Vite) |
|---|---|---|---|
module |
ESNext | CommonJS or Node16 | ESNext |
moduleResolution |
Bundler | Node16 | Bundler |
target |
ES5 or ESNext | ES2022 | ESNext |
extends |
next/core-web-vitals | none (usually) | none (usually) |
2. Turning on strict mode in an existing codebase
If you ask AI to "make this project use TypeScript properly," it might add "strict": true to an existing tsconfig. In a brand new project, that's fine. In a codebase that already has hundreds of files, you'll suddenly get 200 type errors from code that was working fine an hour ago. Strict mode is not retroactive-safe. Always ask AI to add it with the caveat: "add strict mode only if this is a new project with no existing code."
3. Path aliases that work in TypeScript but break at runtime
This one catches a lot of people. You set up paths in tsconfig.json so your imports look like @/utils/logger. TypeScript is happy — no red squiggles. But when you run the compiled JavaScript, you get Cannot find module '@/utils/logger'.
Why? Because tsconfig.json only tells TypeScript's type checker about the aliases. The compiled JavaScript output still has the raw @/utils/logger path, which Node.js has no idea what to do with. You need a separate tool — like tsconfig-paths or tsc-alias — to rewrite the paths in the compiled output. AI often forgets to add this step.
Watch for this: If you set up path aliases and everything looks fine in your editor but crashes at runtime with Cannot find module, this is almost certainly the issue. Tell your AI: "My path aliases work in TypeScript but fail at runtime. Add tsconfig-paths or tsc-alias to rewrite paths in the compiled output."
4. Overly complex configs borrowed from large open-source projects
AI is trained on big codebases. Those codebases have tsconfigs tuned for monorepos, multiple build targets, and edge cases you will never encounter. If AI gives you a tsconfig with 30+ options, a references array, and multiple tsconfig.*.json files for different build modes — ask it to simplify. You almost certainly don't need that level of complexity for a solo project or small team.
When tsconfig Breaks Your Build
Here are the most common symptoms of a broken or misconfigured tsconfig — and what they usually mean:
"Cannot find module '...' or its corresponding type declarations"
This is the classic tsconfig error. It usually means one of three things:
- Your
moduleResolutionsetting doesn't match how your project actually resolves imports - You set up path aliases in
tsconfig.jsonbut didn't set up the runtime resolver - You're importing a library that doesn't have TypeScript types — you may need to install
@types/library-name
"Type 'X' is not assignable to type 'Y'" — but the code looks fine
If you suddenly see a flood of type errors after touching tsconfig, it's almost always because strict mode was turned on (or one of its sub-options). The code isn't wrong — TypeScript is now applying more rigorous checks it wasn't applying before.
"Object is possibly 'undefined'" everywhere
This is strictNullChecks kicking in (it's part of strict: true). TypeScript now requires you to check if a value might be null or undefined before you use it. This is technically correct behavior — it's catching real potential bugs — but it can feel overwhelming if it suddenly appears on 100 lines at once.
The build runs fine but the compiled output is in the wrong place
Check your outDir and rootDir settings. If outDir isn't set, TypeScript compiles files next to your source files (so src/server.ts compiles to src/server.js). If rootDir is wrong, the folder structure inside outDir might be nested differently than you expect.
"Option 'X' cannot be used when 'isolatedModules' is enabled"
Next.js enables isolatedModules: true by default (it's required for fast bundling with Babel or SWC). Some TypeScript features — like const enum — don't work with isolated modules. This usually only matters if you're trying to use an advanced TypeScript feature that Next.js doesn't support, or if you copied an advanced config into a Next.js project.
What to Tell Your AI
Here are ready-to-use prompts for common tsconfig situations:
Starting Fresh
Generate a tsconfig.json for a [Node.js Express / Next.js / React + Vite] project.
I want strict mode enabled, path aliases using @ for the src folder,
and source maps for debugging. Explain each option you include in a comment.
Keep it simple — only include options I actually need.
Fixing Path Alias Errors at Runtime
My TypeScript path aliases work in the editor but fail at runtime with
"Cannot find module '@/utils/something'". My project uses Node.js and tsc to compile.
Fix my tsconfig.json and add whatever tooling is needed to make path aliases
work in the compiled JavaScript output too.
Dealing With a Flood of Strict Mode Errors
I just enabled strict mode in tsconfig.json and now I have 150 TypeScript errors.
I don't want to fix them all right now. Show me how to turn on strict mode
gradually — enabling one check at a time — instead of all at once.
List which strict sub-options to enable first for the most value with the least pain.
Migrating Between Frameworks
I'm moving this project from Next.js to a plain Node.js Express API.
Here is my current tsconfig.json: [paste it here]
Update it for Node.js. Flag any settings that are Next.js-specific and
explain what they should be changed to for a Node.js backend.
The extends Field: Shared Base Configs
One option worth knowing about even if you don't use it right away: extends. It lets your tsconfig inherit from another config file and only override the settings that differ.
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/server"
}
}
You'll see this most often in:
- Next.js projects —
"extends": "next/core-web-vitals"pulls in Next.js's recommended defaults - Monorepos — a shared
tsconfig.base.jsonat the root, with each package extending it - Teams — a shared company-wide tsconfig published as an npm package
For most solo projects, you won't need extends. But when you see it in a generated config, now you know what it's doing: "start with everything from that file, then apply these overrides on top."
include and exclude: What TypeScript Looks At
Two more fields that often cause confusion:
{
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
include tells TypeScript which files to compile. If you don't set it, TypeScript compiles everything it can find. That usually includes files you don't want compiled — like test files, scripts, or legacy code.
exclude tells TypeScript what to skip. node_modules is excluded by default (TypeScript won't compile your dependencies), but dist is not — if you forget to exclude it, TypeScript will try to re-compile its own compiled output, which causes weird duplicate errors.
A common pattern: include only src/ and exclude node_modules, dist, and any test folders you want a separate test tsconfig for.
TypeScript Strict Mode — What It Actually Turns On
Setting "strict": true is a shortcut for enabling eight separate checks at once. You don't need to memorize all of them, but it helps to know the two you'll encounter most:
- strictNullChecks — Variables that could be
nullorundefinedmust be checked before use. This catches null pointer errors before they happen at runtime. It's the most impactful check in strict mode. - noImplicitAny — TypeScript won't silently assign the
anytype when it can't figure out what a variable is. You have to either type it explicitly or TypeScript will error. This pushes you toward better type annotations.
The other six checks (strictFunctionTypes, strictBindCallApply, etc.) are more advanced and mainly relevant when you're writing complex generic code — which AI usually handles for you anyway.
New project? Always start with "strict": true. It's much easier to build with strict mode from the beginning than to bolt it on later. If you're adding TypeScript to an existing JavaScript codebase, turn strict mode on file by file using // @ts-check and // @ts-strict-ignore rather than flipping the global switch.
How tsconfig Relates to Other Config Files
Your project root is probably full of config files by now. Here's how tsconfig relates to the others so you know which one to touch for which problem:
- package.json — manages dependencies and scripts. TypeScript itself is installed as a dependency here (
typescriptin devDependencies). tsconfig is where you configure it. - ESLint (.eslintrc or eslint.config.js) — catches code style and potential logic errors. ESLint and TypeScript both analyze your code but in different ways. You'll often have both. ESLint can use TypeScript's type information via
@typescript-eslint/parser. - next.config.js / vite.config.ts — framework-level configuration. These handle bundling, routing, environment variables. tsconfig handles TypeScript's behavior specifically. They coexist; they don't overlap.
FAQ
What is tsconfig.json?
tsconfig.json is the configuration file that tells the TypeScript compiler how to process your code. It controls things like which JavaScript version to output, how strict the type checking should be, where to find your source files, and where to put the compiled output. Every TypeScript project has one, and AI tools generate it automatically when they scaffold a new project.
Do I need tsconfig.json for JavaScript projects?
Not for pure JavaScript. tsconfig.json is specifically a TypeScript configuration file. However, some JavaScript tools — like VS Code's type checking for plain .js files — can optionally use a tsconfig with "allowJs": true and "checkJs": true enabled. If you're working in a JavaScript-only project with no .ts files, you don't need a tsconfig.json.
What is strict mode in tsconfig?
Setting "strict": true turns on TypeScript's most thorough type checks all at once. The two biggest ones: variables that might be null or undefined must be checked before you use them, and TypeScript will refuse to silently accept any as a type. Strict mode is best practice for new projects but can cause a flood of errors if you switch it on in an existing codebase that wasn't built with it enabled.
What is the paths field in tsconfig?
The paths field lets you create import shortcuts. Instead of writing import Button from '../../../components/ui/Button', you can configure a shortcut like @/components/ui/Button. This makes imports cleaner and easier to refactor. Important caveat: paths only affects TypeScript's type checker — you need a separate runtime tool (tsconfig-paths or tsc-alias) to make those shortcuts work in the compiled JavaScript output.
Should I commit tsconfig.json to version control?
Yes, always. tsconfig.json is not a local preference file — it defines how your project compiles. If someone else clones your repo (or you clone it on a new machine), they need the same tsconfig.json to get the same build behavior. Think of it like your package.json: it belongs in the repo, not in .gitignore.