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

$ terminal
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

JSON tsconfig.json
{
  "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:

OptionDefault nowOld default
stricttruefalse
moduleesnextcommonjs
targetLatest stable ECMAScript versiones5
types[] — list global type packages yourselfEvery @types package found
rootDirThe folder containing tsconfig.jsonInferred from the input files
noUncheckedSideEffectImportstruefalse

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

OptionWhat it controls
targetWhich JavaScript version to emit. Newer syntax is rewritten for older targets. es2022 suits current browsers and Node.
moduleWhich module system the output uses. nodenext for Node, esnext or preserve when a bundler handles modules.
moduleResolutionHow import paths are resolved. bundler for Vite/webpack projects; implied by module: nodenext for Node.
libWhich built-in type declarations exist — e.g. ["es2023", "dom"] for browser code. Defaults are based on target.
outDir / rootDirWhere compiled files go, and which folder structure they mirror.
sourceMapEmit .map files so debuggers and stack traces show your original TypeScript lines.
declarationEmit .d.ts files (for libraries).
noEmitType-check only; write nothing. Common when a bundler does the compiling.
jsxHow 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 become any.
  • strictNullChecks — null and undefined must be handled explicitly (lesson 13).
  • strictFunctionTypes — safer checking of callback parameter types.
  • strictPropertyInitialization — class properties must be initialised.
  • useUnknownInCatchVariables — caught errors are unknown (lesson 37).

Here's what noImplicitAny catches:

TS strict.ts
function total(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
tsc output
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:

OptionCatches
noUncheckedIndexedAccessReading arr[i] or record[key] gives T | undefined
noImplicitOverrideRequires override on overriding methods (lesson 18)
noFallthroughCasesInSwitchA case that falls into the next without break/return
noUnusedLocals / noUnusedParametersDead variables and parameters (often left to a linter instead)
exactOptionalPropertyTypesDistinguishes "property missing" from "property set to undefined"
verbatimModuleSyntaxRequires import type for type-only imports (lesson 31)

Interop options

  • esModuleInterop — lets you write import 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.ts files in node_modules. Faster builds, fewer unfixable errors.
  • resolveJsonModule — allow import data from "./data.json" with full typing.
  • allowJs / checkJs — include and check .js files (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:

JSON tsconfig.json
{
  "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

$ terminal
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
Tip: when a setting seems to be ignored, run npx tsc --showConfig. It prints the configuration after every extends has been applied, which quickly reveals which file is overriding what.