tsconfig.json Explained: The Compiler Options That Matter
tsconfig.json controls how TypeScript checks and compiles your code. Here are the options that actually change behavior, and the ones you can leave alone.
tsconfig.json is the configuration file that tells the TypeScript compiler which files to check, how strictly to check them, and what kind of JavaScript to emit. Most projects inherit one from a starter template and never look at it again, which is a shame, because a handful of its options change how bugs get caught — or don’t — every single day.
The two things tsconfig actually controls
It helps to separate tsconfig.json into two jobs it’s doing at once: type checking (how strict the compiler is about catching mistakes) and emit (what JavaScript, if any, it outputs, and in what module format). Many modern setups — anything built on Vite or Bun — use TypeScript purely for the first job and let another tool handle the second. Knowing which job a given option affects saves a lot of confusion.
Strictness options worth understanding
strict: true is a bundle flag that turns on a set of individually-named checks. Turning it on wholesale is the right default for new projects; the more useful skill is knowing which sub-flags actually caught something, in case you’re auditing an older codebase that has strict: false:
strictNullChecks— without it,nullandundefinedare silently assignable to every type, which defeats most of the point of using TypeScript. This is the single highest-value flag if you can only turn on one.noImplicitAny— flags parameters and variables the compiler can’t infer a type for, instead of quietly falling back toany. This is what actually forces you to write types, rather than just having the option to.strictFunctionTypes— checks function parameter types contravariantly, catching a real (if rare) class of bugs around callback assignment.noUncheckedIndexedAccess— not part ofstrict, but worth turning on separately. It makesarr[i]returnT | undefinedinstead ofT, which is closer to reality: nothing stopsifrom being out of bounds.
Module resolution: the setting that causes the most confusion
moduleResolution and module determine how TypeScript resolves import statements to files, and they need to match your actual runtime, not just look modern. Setting moduleResolution: "bundler" is right for projects built by a bundler that handles resolution itself; "node16" or "nodenext" matches how Node.js actually resolves ESM vs CommonJS at runtime, including the requirement to write file extensions in relative imports. Picking the wrong one produces the maddening class of error where code runs fine but the compiler insists a valid import doesn’t exist, or vice versa.
target and lib: two settings, different meanings
target sets which JavaScript syntax version the compiler emits (or, if emit is disabled, which syntax it assumes the runtime understands for the purpose of type-checking newer language features). lib is separate: it controls which built-in APIs the compiler knows about — Array.prototype.flatMap, Promise.allSettled, DOM types, and so on. It’s common to set a conservative target for broad compatibility while still setting lib to include newer APIs you know your runtime supports. Confusing the two leads to either false “this method doesn’t exist” errors or, worse, code that type-checks but calls a method the target runtime doesn’t actually have.
Project structure: include, exclude, and references
include/exclude scope which files the compiler considers — get this wrong in a monorepo and you’ll either type-check node_modules (slow) or silently skip a package (bugs ship unchecked). For genuine monorepos, references with composite: true lets you split a codebase into project boundaries that each build and check independently, with incremental builds skipping unchanged projects. It’s more setup than a single flat tsconfig.json, and not worth it below a certain project size, but it’s the mechanism, not include globs, that scales to a large monorepo.
A minimal, sane baseline
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"esModuleInterop": true
}
}
skipLibCheck: true is worth calling out separately — it skips type-checking of .d.ts files, including ones in node_modules. It’s not a strictness compromise on your own code; it’s a pragmatic default that avoids grinding on type errors in third-party declaration files you don’t control, and it noticeably speeds up checking in larger projects.
Where tsconfig fits in the toolchain
None of this replaces good types in the code itself — see our guides on generics and utility types for that side of things. tsconfig.json is the dial that decides how much the compiler enforces what you’ve already written. It’s also worth checking how your package manager and runtime interact with it: npm, pnpm, and Yarn don’t read tsconfig.json themselves, but build tools and editors do, and mismatched moduleResolution settings are a frequent source of “works on my machine” bugs across a team.
The takeaway
tsconfig.json is really two configurations bundled into one file: how strictly to check types, and what to emit. Start from strict: true, add noUncheckedIndexedAccess since it isn’t included by default, and make sure moduleResolution actually matches how your code is run rather than copying whatever a template shipped with. Get those three right and most of the confusing “why does this error” moments disappear.
Keep reading
Takina · · 5 min read TypeScript 7.0 Released: Go Rewrite, 10x Faster
Microsoft shipped TypeScript 7.0 with a Go-native compiler that's roughly 10x faster than 6.0. What changed, what breaks, and how to upgrade.
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 4 min read What Is a Lockfile? Reproducible Dependency Installs
A lockfile records the exact dependency versions your package manager resolved, so every install — from your laptop to CI — reproduces the same tree.