tsconfig.json Essentials

intermediate tsconfig compiler-options config

tsconfig.json has 100+ options but realistically we tune maybe 10 of them. In simple language, this file tells the TS compiler what flavor of JS to emit, how strict to be, and where to look for files. Get the essentials right and the rest can stay default.

The shape

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

That’s a sensible modern config. Let’s go through what each line actually controls.

target — which JS version to emit

target decides what JS version the compiler outputs. Set it to the lowest version we need to support. Modern projects (Node 18+, evergreen browsers) can safely use ES2022 or higher.

"target": "ES2022"

If we set target: "ES5", TS will down-compile async/await to generator-based polyfills and class to function constructors. Painful and unnecessary in 2025.

module & moduleResolution — the import system

These two work together. module decides the output import style. moduleResolution decides how the compiler finds imported files.

"module": "ESNext",          // emit ES module syntax
"moduleResolution": "bundler" // resolve like Vite/webpack do

Quick guide:

  • App in Vite/webpack/esbuild → module: "ESNext", moduleResolution: "bundler"
  • Native Node ESM → module: "Node16", moduleResolution: "Node16" (requires .js extensions in imports)
  • Old Node + CJS → module: "CommonJS", moduleResolution: "Node"

strict — turn on safety

This is the most important flag. "strict": true enables a bundle of checks that catch real bugs. Always enable it.

"strict": true

Behind the scenes, that bundle includes:

  • noImplicitAny — no untyped parameters
  • strictNullChecks — null and undefined are not assignable to other types
  • strictFunctionTypes — proper variance checking
  • strictBindCallApply — type-check bind/call/apply
  • strictPropertyInitialization — class properties must be assigned in constructor
  • noImplicitThisthis can’t be implicitly any
  • alwaysStrict — emit "use strict"
  • useUnknownInCatchVariablescatch (e) is unknown, not any

Migrating an old codebase? Turn strict on and then disable specific sub-flags one at a time. Better than living with no checks.

Strict family — what each flag catches
noImplicitAny → no untyped params
strictNullChecks → null/undefined safety
strictFunctionTypes → callback variance
strictBindCallApply → typed .bind
strictPropertyInit → class fields init
noImplicitThis → typed this
alwaysStrict → "use strict"
useUnknownInCatch → safe catch

paths & baseUrl — clean imports

Tired of import x from "../../../../utils"? paths lets us define aliases.

"baseUrl": ".",
"paths": {
  "@/*": ["src/*"],
  "@components/*": ["src/components/*"]
}

Now we can write import { Button } from "@components/Button". One important catch: TS only uses paths for type-checking — the runtime still needs a bundler or tsconfig-paths to resolve those aliases. If we’re using Vite/Next/webpack, they have their own config to set up matching aliases.

skipLibCheck — pragmatic shortcut

"skipLibCheck": true

Skips type-checking inside node_modules/@types/*. Almost everyone enables this — it speeds up builds and avoids fights with third-party type errors we can’t fix anyway.

esModuleInterop — CJS / ESM bridge

"esModuleInterop": true

Lets us write import express from "express" instead of import * as express from "express" for CommonJS modules. Modern default — leave it on.

include, exclude, files

Outside compilerOptions, we control which files are part of the project.

"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]

include defaults to everything under the tsconfig’s folder. exclude defaults to node_modules, bower_components, jspm_packages, and the outDir. Use files for an explicit list (rare).

Useful extras worth knowing

A few more flags that pay off in real projects:

"noUncheckedIndexedAccess": true,  // arr[0] becomes T | undefined — huge for safety
"exactOptionalPropertyTypes": true, // { x?: T } can't be { x: undefined }
"noImplicitOverride": true,        // require `override` keyword on subclass methods
"verbatimModuleSyntax": true,      // enforce `import type` for type-only imports
"resolveJsonModule": true,         // import data.json
"isolatedModules": true            // each file compilable alone (needed for esbuild/swc)

noUncheckedIndexedAccess is the underrated banger — it makes array and record access return T | undefined, forcing us to handle the empty case. Catches a class of bugs that strictNullChecks misses.