Compiling with tsconfig
A tsconfig.json file marks the root of a TypeScript project and tells the compiler which files to include, what JavaScript to produce, and how strict to be. Understanding a dozen of its options covers almost every project you'll meet.
Creating one
npx tsc --init
This writes a short tsconfig.json with modern, strict settings. Once it exists, run npx tsc with no file names: the compiler finds the config and builds the whole project.
A typical config
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUncheckedIndexedAccess": true,
"types": ["node"],
"skipLibCheck": true,
"sourceMap": true
},
"include": ["src"]
}
The file has two main parts: compilerOptions, which controls checking and output, and top-level fields such as include, exclude and files, which say which files belong to the project.
Defaults changed in TypeScript 6 and 7
TypeScript 6.0 (March 2026) modernised the defaults, and 7.0 (July 2026) removed many legacy options entirely. If you read an older tutorial or open an older project, these are the differences that matter:
| Option | Default now | Old default |
|---|---|---|
strict | true | false |
module | esnext | commonjs |
target | Latest stable ECMAScript version | es5 |
types | [] — list global type packages yourself | Every @types package found |
rootDir | The folder containing tsconfig.json | Inferred from the input files |
noUncheckedSideEffectImports | true | false |
Removed in 7.0 (they were deprecated in 6.0): target: es5, downlevelIteration, moduleResolution: node/node10/classic, module: amd/umd/systemjs/none, baseUrl, outFile, and setting esModuleInterop or allowSyntheticDefaultImports to false. In 6.0, "ignoreDeprecations": "6.0" temporarily silences these warnings while you migrate.
Even though strict is now the default, it's still worth writing "strict": true explicitly — it documents intent and keeps the config correct if someone uses an older compiler.
Output options
| Option | What it controls |
|---|---|
target | Which JavaScript version to emit. Newer syntax is rewritten for older targets. es2022 suits current browsers and Node. |
module | Which module system the output uses. nodenext for Node, esnext or preserve when a bundler handles modules. |
moduleResolution | How import paths are resolved. bundler for Vite/webpack projects; implied by module: nodenext for Node. |
lib | Which built-in type declarations exist — e.g. ["es2023", "dom"] for browser code. Defaults are based on target. |
outDir / rootDir | Where compiled files go, and which folder structure they mirror. |
sourceMap | Emit .map files so debuggers and stack traces show your original TypeScript lines. |
declaration | Emit .d.ts files (for libraries). |
noEmit | Type-check only; write nothing. Common when a bundler does the compiling. |
jsx | How to handle JSX in .tsx files — usually react-jsx. |
The strict flag
"strict": true is the single most important setting. It switches on a family of checks, including:
noImplicitAny— error when a type can't be inferred and would silently becomeany.strictNullChecks—nullandundefinedmust be handled explicitly (lesson 13).strictFunctionTypes— safer checking of callback parameter types.strictPropertyInitialization— class properties must be initialised.useUnknownInCatchVariables— caught errors areunknown(lesson 37).
Here's what noImplicitAny catches:
function total(items) { return items.reduce((sum, item) => sum + item.price, 0); }
error TS7006: Parameter 'items' implicitly has an 'any' type.
Without strict mode this compiles, and items is silently any — no checking at all. Keep strict on for every project. Future TypeScript versions may add new checks under strict, which is intentional: you get them automatically.
Extra checks worth enabling
These aren't part of strict but catch real bugs:
| Option | Catches |
|---|---|
noUncheckedIndexedAccess | Reading arr[i] or record[key] gives T | undefined |
noImplicitOverride | Requires override on overriding methods (lesson 18) |
noFallthroughCasesInSwitch | A case that falls into the next without break/return |
noUnusedLocals / noUnusedParameters | Dead variables and parameters (often left to a linter instead) |
exactOptionalPropertyTypes | Distinguishes "property missing" from "property set to undefined" |
verbatimModuleSyntax | Requires import type for type-only imports (lesson 31) |
Interop options
esModuleInterop— lets you writeimport express from "express"for CommonJS packages that don't have a real default export. Always on since TypeScript 7; you'll still see it written out in older configs.skipLibCheck— skip type-checking.d.tsfiles innode_modules. Faster builds, fewer unfixable errors.resolveJsonModule— allowimport data from "./data.json"with full typing.allowJs/checkJs— include and check.jsfiles (lesson 42).
Sharing settings with extends
A config can inherit from another and override only what differs. Community-maintained base configs exist for most environments:
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
Install the base with npm i -D @tsconfig/node22. Monorepos often keep one tsconfig.base.json at the root that every package extends.
Project references
In large repositories, "references" splits the code into separately compiled sub-projects. tsc --build then only rebuilds projects whose files changed, which can turn minute-long builds into seconds. Each referenced project needs "composite": true.
Useful commands
npx tsc # build the project npx tsc --noEmit # type-check only npx tsc --watch # rebuild on save npx tsc -p tsconfig.build.json # use a different config file npx tsc --showConfig # print the final merged configuration
npx tsc --showConfig. It prints the configuration after every extends has been applied, which quickly reveals which file is overriding what.