tsconfig explained, option by option
Every TypeScript project has a tsconfig.json — the file that tells the compiler how to behave. Most
people copy one from a template and never read it, then are baffled when the compiler does something
unexpected. This lesson demystifies the options that actually matter, so you can configure a project
deliberately rather than by superstition. You do not need to memorise all of them; you need to
understand the dozen that shape your daily experience.
What tsconfig.json is
tsconfig.json sits at a project's root and configures tsc (and, through it, your editor). Its presence
defines a TypeScript project — the compiler finds it and applies its settings to every file. You create
one with npx tsc --init, which generates a heavily-commented default. The structure:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
compilerOptions is the bulk of it — how to check and compile. include/exclude say which files. Let us
walk the options that matter, grouped by what they control.
Type-checking strictness — the most important group
These decide how hard TypeScript checks you, and getting them right is the difference between real safety and a false sense of it:
"strict": true, // turn on ALL strict checks — always do this
"noUncheckedIndexedAccess": true, // arr[i] is T | undefined — safer, catches out-of-bounds
"noImplicitOverride": true, // require 'override' keyword — catches renamed base methods
"exactOptionalPropertyTypes": true, // distinguish "absent" from "undefined" precisely
strict: true is the single most important line in the file — it enables noImplicitAny,
strictNullChecks, and the whole family of strict checks. Turn it on from day one; the next lesson is
entirely about why. The others (noUncheckedIndexedAccess especially) are extra strictness beyond
strict that catches real bugs — noUncheckedIndexedAccess makes arr[i] return T | undefined,
forcing you to handle the out-of-bounds case, which prevents a genuine class of crash. Turning on the
extra-strict options is a mark of a project that takes types seriously.
Output — target, module, and where JS goes
These control what JavaScript is produced:
"target": "ES2022", // which JS version to compile DOWN to
"module": "NodeNext", // which module system (import/export style) to emit
"outDir": "./dist", // where the compiled .js files go
"sourceMap": true, // emit .map files so debuggers point at your .ts
"declaration": true, // emit .d.ts type files (for a library others consume)
target— the JavaScript version to emit.ES2022(or newer) is fine for modern Node and browsers; a lower target (ES2015) down-compiles newer syntax for older environments, at the cost of larger output. Match it to where your code runs.module— how imports/exports are emitted.NodeNext/ESNextfor modern setups; a bundler (Vite, webpack) or framework usually dictates this.outDir— wheretscwrites the compiled.js. Keeps output separate from source.sourceMap— emit source maps so debugging shows your TypeScript, not the compiled JS. Turn it on for development.declaration— emit.d.tsfiles describing your types, for when you publish a library others import (the declaration-files lesson). Off for an application, on for a library.
Note: if you use a bundler (Vite, esbuild) or tsx, they often handle the actual compilation, and tsc is
used only for type-checking (--noEmit) — so target/module/outDir matter less, and the strictness
options matter most. Know which role tsc plays in your setup.
Environment — lib and types
These tell TypeScript what globals exist in your runtime:
"lib": ["ES2022", "DOM"], // which built-in APIs are available
"types": ["node"], // which @types packages to include globally
lib— the built-in APIs TypeScript knows about.DOMaddsdocument,window,fetch(browser); omit it for a pure Node server (or you will get browser globals that do not exist).ES2022adds newer JS methods (Array.at,Object.hasOwn). A "Cannot find name 'document'" error usually meansDOMis missing fromlib; a "Cannot find name 'Promise'" means yourlibis too old.types— restricts which@types/*packages are loaded globally. Usually you leave it out (all are loaded); set it to exclude unwanted ambient types.
Getting lib right for your environment is a common source of confusing errors — a browser project needs
DOM, a Node project does not (it uses @types/node).
Module resolution — how imports are found
"moduleResolution": "Bundler", // or "NodeNext" — how to resolve import paths
"baseUrl": ".", // base for non-relative imports
"paths": { "@/*": ["src/*"] }, // import aliases: @/utils instead of ../../utils
"esModuleInterop": true, // smooth interop with CommonJS modules
"resolveJsonModule": true, // allow importing .json files
moduleResolution— how the compiler finds a module from an import path.Bundlerfor bundler-based projects,NodeNextfor Node. This must match your runtime, or imports resolve wrongly.paths/baseUrl— import aliases, soimport { x } from "@/utils"works instead of a long relative../../../utils. A nice quality-of-life setting (your bundler must be configured to match).esModuleInterop— makesimport express from "express"work smoothly with older CommonJS modules; almost alwaystrue.resolveJsonModule— lets youimport data from "./data.json"with types inferred.
The settings that matter most, in priority order
If you read nothing else in a tsconfig, check these:
strict: true— the most important line; enables real type safety.targetandmodule— must match where your code runs.lib— must include your runtime's globals (DOMfor browser, not for Node).moduleResolution— must match your setup, or imports break.noUncheckedIndexedAccess— extra safety worth enabling.
Most other options are situational. The mistake to avoid is copying a random tsconfig and being confused by
its behaviour — now you can open the file, read the options that matter, and understand why the compiler
behaves as it does. When something surprising happens (a global not found, an import not resolving, a check
not firing), the answer is almost always in lib, moduleResolution, or strict.
Check your work
What tsconfig.json is. The file configuring tsc (and your editor); its presence defines a
TypeScript project.
The most important option. strict: true — enables the whole family of strict checks; always on.
What extra-strict options add. Beyond strict, options like noUncheckedIndexedAccess (arr[i] is
T | undefined) catch more real bugs.
What target controls. The JavaScript version emitted — match it to where the code runs.
What lib controls, and a common error. Which built-in globals TypeScript knows — DOM for browser
(gives document, fetch), omitted for Node; "Cannot find name 'document'" means DOM is missing.
What moduleResolution controls. How import paths are resolved — must match your runtime or imports
break.
What paths/baseUrl give. Import aliases (@/utils) instead of long relative paths.
The role tsc plays with a bundler. Often just type-checking (--noEmit), while the bundler compiles —
so strictness options matter most.
Practice
- Run
npx tsc --initand read the generated, commented tsconfig. Findstrict,target,lib,module. - Turn
strictoff, write code with an implicitanyand a possible null, and confirm they are not caught. Turn it back on and watch them error. - Remove
DOMfromlibin a project usingdocument, and read the error. Add it back. - Enable
noUncheckedIndexedAccessand index an array; confirm the result isT | undefined. - Set up a
pathsalias (@/*) and import with it instead of a relative path. - Set a lower
target(ES2015), compile code using a newer method, and compare the emitted JS. - For a project you know, decide the right
target,lib, andmoduleResolutionfor its runtime, and justify each.
Official documentation
- TypeScript — tsconfig reference — Every option, searchable — the definitive reference.
- TypeScript — What is a tsconfig.json — The file's role and structure.
- tsconfig bases — Sensible starting configs per runtime, to extend.
Next: strict mode, and why to turn it on from day one.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship