tsconfig.json Configuration

tsconfig.json tells the TypeScript compiler which files belong to the project, how strictly to check them, what JavaScript and module format to produce (if any), and how to resolve imports. Most "TypeScript is weird" moments trace back to it: imports that work in the editor but fail at runtime, strict switched off years ago, or path aliases the bundler understands and Node doesn't.

The option list is long, but only a dozen or so settings matter for most projects. This page explains those and gives sensible baselines for the common setups.

TL;DR

Quick Example

A strict baseline for a bundled web app (Vite or Next.js style):

tsc --noEmit (often npm run typecheck) then checks the whole project in CI, while Vite transpiles each file independently during development.

Core Concepts

Strictness

"strict": true enables a family of checks, including:

Worth adding on top:

Target and Lib

target controls syntax downleveling: with ES2017, optional chaining gets compiled to older code. lib controls which APIs TypeScript believes exist: including "DOM" types document and window, and "ES2023" types Array.prototype.findLast. lib doesn't polyfill anything; it only declares types. For Node projects, omit "DOM" and install @types/node.

Module and Module Resolution

These decide how import statements are interpreted and resolved:

Picking the wrong mode is the root of "works in the editor, breaks at runtime" import bugs. Match the mode to whatever actually executes or bundles your code.

Emit or No Emit

isolatedModules and verbatimModuleSyntax

Single-file transpilers (esbuild, SWC, Babel, Node's type stripping) compile each file without seeing the others, so they can't tell whether an import is a type or a value. isolatedModules flags code that depends on cross-file knowledge. verbatimModuleSyntax goes further: imports are kept or dropped exactly as written, so type-only imports must use import type.

Project Structure

include, exclude, files

include lists globs of source files; exclude removes some (it defaults to node_modules and outDir). Test files often get a separate config that extends the main one and adds test globals.

extends

Share settings through inheritance: "extends": "./tsconfig.base.json" or community presets like @tsconfig/strictest and @tsconfig/node22. Child configs override individual options.

Project References

In monorepos, references plus "composite": true split the codebase into sub-projects with explicit dependencies. tsc --build then compiles incrementally and only re-checks what changed. It's faster for large repos, at the cost of more configuration.

Best Practices

Start Strict, Stay Strict

Enabling strict on a new project costs nothing; retrofitting it later costs weeks. For an existing loose project, turn on flags one at a time and fix errors incrementally, beginning with strictNullChecks.

Type-Check in CI Separately From Building

Fast transpilers skip type-checking entirely. Run tsc --noEmit (or tsc -b) as its own CI step so type errors fail the build.

Keep Aliases in One Place

If you use paths, make sure the bundler (Vite's resolve.alias, or vite-tsconfig-paths), test runner, and runtime resolve the same aliases. Node's package.json "imports" field (#src/*) is a standards-based alternative that both TypeScript and Node understand.

Use skipLibCheck

It skips type-checking third-party .d.ts files. That's much faster, and it avoids errors in dependencies you can't fix. Your own code is still fully checked.

Common Mistakes

paths Without Runtime Support

Configure the bundler or runtime to resolve the alias, or use Node subpath imports.

Mixing ESM Output With CommonJS Package Settings

With "module": "nodenext", whether a file is ESM or CJS depends on package.json "type" and the file extension (.mts/.cts). Mismatches produce require is not defined or ERR_REQUIRE_ESM at runtime. Decide on ESM and set "type": "module".

Disabling Checks to Silence Errors

Setting strict: false or noImplicitAny: false to make an upgrade compile trades a handful of visible errors for hundreds of invisible bugs. Fix or locally suppress with // @ts-expect-error and a comment instead.

FAQ

Should I use moduleResolution: "bundler" or "nodenext"?

Use bundler when a bundler or a runtime like Bun processes your imports, which covers most frontend and full-stack apps. Use nodenext when Node itself loads your compiled output, as with libraries published to npm and Node services compiled with tsc. Libraries should use nodenext so they work for every consumer.

Why does TypeScript want .js extensions in my imports?

Under nodenext with ESM, Node requires full file paths, and TypeScript doesn't rewrite import specifiers. You write the path of the output file (./util.js) even though the source is util.ts. With rewriteRelativeImportExtensions (TS 5.7+) you can write .ts and have it rewritten on emit.

What does skipLibCheck hide?

Type errors inside .d.ts files, mostly from node_modules. It doesn't affect checking of your own .ts files. Nearly every project enables it for speed and to avoid conflicts between dependency type definitions.

Do I need a separate tsconfig for tests?

Often, yes. A tsconfig.test.json that extends the base and adds test-runner types ("types": ["vitest/globals"]) keeps test globals out of your app code. Many frameworks generate separate app, node, and test configs for exactly this reason.

Related Topics

References