cheat sheet
tsconfig.json Reference
Complete reference for tsconfig.json — compiler options for type checking, module resolution, output, paths, and JSX. Includes ready-to-use presets for Node 20, browser libraries, and Vite/React apps.
tsconfig.json Reference
What it is
tsconfig.json is the TypeScript project configuration file placed at the root of a TypeScript project. It controls which files are included in the compilation and how the compiler processes them. Options are split across two concerns: which files to compile (include, exclude, files, references) and how to compile them (compilerOptions).
Basic structure
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"outDir": "dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": []
}
Top-level fields
| Field | Purpose |
|---|---|
compilerOptions | Compiler behavior settings |
include | Glob patterns for files to include |
exclude | Glob patterns to exclude (defaults: node_modules, outDir) |
files | Explicit list of files (overrides include) |
extends | Path to a base tsconfig to inherit from |
references | Sub-project references for tsc --build |
Type checking options
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
}
}
Key options explained
strict — Enables a bundle of strict checks. Equivalent to setting all of the following to true: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables. Always enable this.
noImplicitAny — Errors when TypeScript infers type any due to missing annotations. Forces explicit typing.
strictNullChecks — Makes null and undefined distinct types. Without this, every type is implicitly nullable — a common source of runtime bugs.
strictFunctionTypes — Enforces stricter checking of function parameter types (contravariance). Catches subtle callback type errors.
noUncheckedIndexedAccess — Array index access returns T | undefined instead of T. Prevents silent out-of-bounds bugs.
// noUncheckedIndexedAccess: true
const arr = [1, 2, 3];
const first = arr[0]; // type: number | undefined
if (first !== undefined) {
console.log(first.toFixed(2)); // safe
}
exactOptionalPropertyTypes — Distinguishes between a property being absent and being explicitly set to undefined.
// exactOptionalPropertyTypes: true
interface Config {
timeout?: number;
}
const c: Config = { timeout: undefined }; // Error — must omit the key instead
Module and output options
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false
}
}
module values
| Value | Use case |
|---|---|
CommonJS | Node.js with require() |
ESNext | Bundlers (Vite, esbuild, Webpack) |
NodeNext | Node.js with native ESM (.mjs / "type": "module") |
Node16 | Same as NodeNext but pinned to Node 16 semantics |
Preserve | Keep the module format as authored (TS 5.4+) |
moduleResolution values
| Value | Matches |
|---|---|
node | Classic Node.js require() resolution (legacy) |
bundler | Vite, esbuild, Webpack — allows extensionless imports |
node16 / nodenext | Native Node.js ESM — requires explicit .js extensions in imports |
module: "NodeNext"requires import paths in your source to use.jsextensions even though the source files are.ts. This is because Node.js resolves the compiled output, not the source.
target values
Sets the JavaScript version of the emitted output. TypeScript will down-compile any syntax newer than the target.
| Value | Minimum runtime |
|---|---|
ES2020 | Node 14+, modern browsers |
ES2022 | Node 16+, modern browsers |
ESNext | Latest supported features (tracks TS release) |
Declaration and map options
| Option | Effect |
|---|---|
declaration | Emit .d.ts type declaration files |
declarationMap | Emit .d.ts.map files (go-to-definition jumps to source) |
sourceMap | Emit .js.map for runtime debugging |
inlineSourceMap | Embed source map inside .js instead of separate file |
Path and import options
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@app/*": ["src/app/*"],
"@lib/*": ["src/lib/*"],
"@/*": ["src/*"]
},
"rootDirs": ["src", "generated"]
}
}
baseUrl — The base directory for non-relative module names. Usually set to . (project root) or src.
paths — Map import aliases to file paths. Must be used alongside a bundler or a path resolver plugin (tsconfig-paths) at runtime since tsc does not rewrite import paths.
rootDirs — Tells TypeScript to treat multiple directories as a single virtual root. Useful when generated files live in a separate directory.
JSX
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
| Value | Description |
|---|---|
react | Classic React.createElement transform (React 16 and older) |
react-jsx | New JSX transform (React 17+, no import required) |
react-jsxdev | Same as react-jsx with dev runtime (extra warnings) |
preserve | Keep JSX as-is for a bundler to handle (Vite, esbuild) |
For Solid.js: "jsxImportSource": "solid-js/h". For Preact: "jsxImportSource": "preact".
Extending a base config
The extends field inherits all options from another tsconfig and allows overriding individual fields.
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}
Install community base configs from npm:
npm install -D @tsconfig/node20
npm install -D @tsconfig/strictest
npm install -D @tsconfig/vite-react
Output: (none — exits 0 on success)
Common presets
Node 20 application
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Browser library (published to npm)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"strict": true,
"noUncheckedIndexedAccess": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Vite + React application
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
tsconfig.node.json (for Vite config file itself):
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"composite": true
},
"include": ["vite.config.ts"]
}
For Vite projects, set
noEmit: truein the main tsconfig — Vite handles the actual compilation. Usetsc --noEmitin CI only for type-checking.
Complete option quick-reference
{
"compilerOptions": {
// Type checking
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// Module
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
// Output
"target": "ES2022",
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Paths
"baseUrl": ".",
"paths": { "@/*": ["src/*"] },
// Misc
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
How tsc reads tsconfig.json
When you run tsc with no arguments, the compiler walks up from the current working directory looking for a tsconfig.json. Once found, it parses the JSON, resolves extends chains, expands include/exclude/files globs, and finally invokes the compilation. Knowing the exact order helps debug missing-file errors and surprise emit shapes.
npx tsc --showConfig
Output:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"skipLibCheck": true
},
"files": [],
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
--showConfig is the canonical way to inspect what tsc actually sees — after extends resolution and after every glob is expanded. If your build emits the wrong shape, this is the first command to run.
To run tsc against a different config without changing directories:
npx tsc -p tsconfig.build.json
Output: (none — exits 0 on success)
The -p (project) flag accepts a directory (looking for tsconfig.json inside) or a direct path to any .json file.
Type checking deep dive
Each of the strict-family flags addresses a specific category of subtle bugs. Understanding what each one catches makes it easier to migrate a legacy codebase one flag at a time.
strict: the bundle
strict: true is shorthand for enabling seven sub-flags simultaneously. The list grows occasionally — TypeScript may add a new strict check in a future release. Enabling strict opts in to whatever the current TypeScript version considers strict.
{
"compilerOptions": {
"strict": true
}
}
Equivalent to setting all of these explicitly:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true
}
}
For migrating an existing JS codebase, enable strict: true then disable individual flags as needed: "strict": true, "noImplicitAny": false. This is the recommended approach over enabling flags one by one — it lets future TS versions add new strict checks that your team has opted into.
useUnknownInCatchVariables
Before TS 4.4, catch (e) typed e as any. With useUnknownInCatchVariables: true (part of strict), e is unknown, forcing a runtime narrow before use.
try {
doSomething();
} catch (e) {
if (e instanceof Error) {
console.error(e.message);
} else {
console.error("Unknown error:", e);
}
}
Without the flag, e.message would compile without checks and crash if e is a number or null. The narrow is required for type safety.
strictPropertyInitialization
Class fields must be initialised in the constructor or have an initialiser:
class User {
// Error — strictPropertyInitialization
name: string;
// OK — definite assignment assertion
email!: string;
// OK — initialiser
role: string = "member";
// OK — initialised in constructor
id: number;
constructor() {
this.id = Math.random();
}
}
The ! (definite assignment assertion) tells TS "I promise this is initialised before any read". Use sparingly — it's an escape hatch.
exactOptionalPropertyTypes
Without this flag, an optional property name?: string is treated as string | undefined. With it, the property must either be absent or hold a string — explicitly passing undefined errors.
interface Config { timeout?: number; }
// Default (exactOptionalPropertyTypes: false)
const a: Config = { timeout: undefined }; // OK
// Strict (exactOptionalPropertyTypes: true)
const b: Config = { timeout: undefined };
// Error TS2375: Type '{ timeout: undefined; }' is not assignable to type 'Config'
// with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type
// of the target.
const c: Config = {}; // OK — absent
const d: Config = { timeout: 1000 }; // OK — present with value
This flag catches a common bug: { ...defaults, timeout: undefined } silently overrides the default. Most teams enable it; some find it too noisy on existing codebases.
noUncheckedIndexedAccess
Index access (arr[0], obj["key"]) returns T | undefined instead of T. Prevents silent out-of-bounds bugs.
// noUncheckedIndexedAccess: false (default)
const arr = [1, 2, 3];
const first = arr[100]; // type: number, value: undefined — silent bug
// noUncheckedIndexedAccess: true
const arr2 = [1, 2, 3];
const first2 = arr2[100]; // type: number | undefined — must narrow
if (first2 !== undefined) {
console.log(first2.toFixed(2));
}
Pair this with Object.entries() and Object.keys() for safer iteration:
const record: Record<string, number> = { a: 1, b: 2 };
const val = record["c"]; // type: number | undefined
noImplicitOverride
Forces explicit override keyword on method overrides in subclasses. Catches typo-renamed methods that silently no longer override anything.
class Base {
greet() { return "hi"; }
}
class Child extends Base {
// Error — missing 'override' keyword
greet() { return "hello"; }
// OK
override greet() { return "hello"; }
}
If you rename Base.greet to Base.greetUser, every Child.greet becomes a new method instead of an override — noImplicitOverride catches this immediately.
Other useful checks
| Flag | Catches |
|---|---|
noImplicitReturns | A function with mixed return / no return paths |
noFallthroughCasesInSwitch | Missing break in switch cases |
noUnusedLocals | Declared-but-unused local variables |
noUnusedParameters | Declared-but-unused function parameters (prefix with _ to allow) |
allowUnreachableCode: false | Code after return / throw |
allowUnusedLabels: false | Labels not referenced by any break/continue |
noPropertyAccessFromIndexSignature | obj.dynamic when only obj["dynamic"] should be allowed |
{
"compilerOptions": {
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true
}
}
Source maps and declaration files
The emit options control what tsc writes to disk alongside the compiled JavaScript. Each artifact has a distinct purpose.
Source maps
A source map is a .js.map file that maps positions in the compiled JavaScript back to positions in the original TypeScript. Browsers, Node debuggers, and stack-trace tools read it to show the original .ts in error messages.
{
"compilerOptions": {
"sourceMap": true
}
}
tsc
ls dist/
Output:
index.js
index.js.map
util.js
util.js.map
| Option | Effect |
|---|---|
sourceMap: true | Emit external .js.map file |
inlineSourceMap: true | Embed source map at the bottom of .js as a data URL |
inlineSources: true | Include the original .ts content in the source map itself |
sourceRoot: "/src" | Prefix in the map's sources array (useful when serving from a CDN) |
mapRoot: "/maps" | Tell consumers where to fetch the .map files from |
In Node, run with --enable-source-maps to get TS line numbers in stack traces:
node --enable-source-maps dist/index.js
Output:
Error: Boom
at greet (src/index.ts:7:9)
at main (src/index.ts:12:3)
Without the flag, the stack would point at dist/index.js.
Declaration files
declaration: true emits a .d.ts for every .ts source file. These are the "headers" that downstream consumers see when they import your library.
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": false
}
}
| Option | Effect |
|---|---|
declaration | Emit .d.ts files |
declarationMap | Emit .d.ts.map — lets editors "go to definition" jump to source |
emitDeclarationOnly | Skip .js emit entirely (for projects that bundle JS separately) |
declarationDir | Override the output directory for .d.ts (defaults to outDir) |
A library's typical declaration-emit setup:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
}
}
tsc
ls dist/
Output:
index.d.ts
index.d.ts.map
index.js
index.js.map
util.d.ts
util.d.ts.map
util.js
util.js.map
emitDeclarationOnly: true is useful when a faster bundler (esbuild, swc, Bun) handles JS emit but you still need TypeScript's .d.ts:
# Emit only .d.ts files
tsc --emitDeclarationOnly
# Build JS with a fast bundler in parallel
esbuild src/index.ts --bundle --outdir=dist --format=esm
Output: (none — exits 0 on success)
JSX deep dive
The jsx option controls how .tsx files are compiled. Each value produces dramatically different output.
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
jsx value | Output for <div /> | When to use |
|---|---|---|
react | React.createElement('div') | React 16 and older |
react-jsx | import { jsx } from 'react/jsx-runtime'; jsx('div') | React 17+ (no React import needed in source) |
react-jsxdev | Same as react-jsx with dev runtime | Development builds with line/column tracking |
preserve | <div /> (untouched) | Vite/esbuild that handles JSX downstream |
react-native | <div /> (untouched, but .js extension) | React Native bundlers |
The jsxImportSource option is for non-React JSX runtimes:
// Preact
{ "jsx": "react-jsx", "jsxImportSource": "preact" }
// Solid
{ "jsx": "preserve", "jsxImportSource": "solid-js" }
// Emotion (classic JSX)
{ "jsx": "react", "jsxFactory": "jsx", "jsxFragmentFactory": "Fragment" }
For Astro projects, jsx: "preserve" is the default — Astro's own preprocessor handles .astro and JSX in one pass.
Decorators and metadata
Decorators live in their own compilation pass. Two distinct decorator systems exist; the compiler options differ.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": true
}
}
| Option | When to use |
|---|---|
experimentalDecorators: true | Legacy decorator API — Angular, NestJS, TypeORM, class-validator |
| (no flag) | TC39 standard decorators (TS 5.0+) — modern framework-agnostic code |
emitDecoratorMetadata: true | Stores parameter types at runtime via Reflect.metadata; requires experimentalDecorators |
useDefineForClassFields: true | Use ES2022 class-field semantics (define not Object.assign) |
You cannot mix the legacy and standard APIs in the same project. See decorators for the full comparison.
Project references in detail
references and composite: true together turn a single tsconfig into a graph of incrementally-built sub-projects. The compiler can skip whole projects whose inputs are unchanged, dramatically speeding up monorepo builds.
// tsconfig.json (root)
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/ui" },
{ "path": "./packages/api" },
{ "path": "./apps/web" }
]
}
Each referenced project sets composite: true:
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"]
}
Build the whole graph in dependency order:
npx tsc --build
Output:
(no output — exit code 0)
See project references for the deep dive on --build, .tsbuildinfo caching, and Turborepo/Nx integration.
The @tsconfig/* ecosystem
Maintaining a custom tsconfig per project is repetitive. The @tsconfig/* packages on npm are community-maintained presets you can extends from.
| Package | Targets |
|---|---|
@tsconfig/node20 | Node 20 with NodeNext modules and strict checks |
@tsconfig/node22 | Node 22 with the latest module settings |
@tsconfig/strictest | Every strict flag turned on, including noUncheckedIndexedAccess and exactOptionalPropertyTypes |
@tsconfig/recommended | Sensible defaults; less aggressive than strictest |
@tsconfig/vite-react | Vite + React app with jsx: "react-jsx" and moduleResolution: "bundler" |
@tsconfig/next | Next.js apps |
@tsconfig/remix | Remix apps |
@tsconfig/svelte | Svelte projects |
@tsconfig/deno | Deno scripts |
@tsconfig/bun | Bun-first projects |
@tsconfig/cloudflare-workers | Cloudflare Workers with WebWorker lib and Bundler resolution |
@tsconfig/create-react-app | CRA-compatible (legacy) |
Install and extend:
npm install -D @tsconfig/node20 @tsconfig/strictest
Output:
added 2 packages in 0.6s
{
"extends": ["@tsconfig/node20/tsconfig.json", "@tsconfig/strictest/tsconfig.json"],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
The right-most extends wins on conflicts (TS 5.0+). Layer presets: a base (@tsconfig/node20) plus a strictness modifier (@tsconfig/strictest).
Verify the merged config:
npx tsc --showConfig | head -20
Output:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
paths and bundler agreement
paths aliases (@/components/Button → src/components/Button.ts) only affect TypeScript's type-checking — the emitted JavaScript still contains the literal alias. At runtime, your bundler (or a path-resolver shim) must agree on the same mapping or the import will fail.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@lib/*": ["src/lib/*"]
}
}
}
The four runtime stories:
- Vite — mirror in
vite.config.ts:
import { defineConfig } from "vite";
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
"@components": fileURLToPath(new URL("./src/components", import.meta.url)),
"@lib": fileURLToPath(new URL("./src/lib", import.meta.url)),
},
},
});
- esbuild — use the
tsconfig-pathsplugin:
npm install -D esbuild esbuild-plugin-tsconfig-paths
Output:
added 2 packages in 0.4s
- Node directly — use
tsconfig-paths/register:
node --import tsconfig-paths/register dist/index.js
Output: (none — exits 0 on success)
- Bun — Bun reads
tsconfig.jsonpathsnatively:
bun run src/index.ts
Output:
Hello from TypeScript
If paths is set but the bundler doesn't know about it, you get a runtime Cannot find module '@/foo' error — code that type-checks but doesn't run. Always keep the two sides in sync, or use workspaces instead (see project references).
skipLibCheck — the universal escape hatch
skipLibCheck: true is the single most-recommended option for shipping projects. It tells tsc not to type-check .d.ts files in node_modules — only your source.
{
"compilerOptions": {
"skipLibCheck": true
}
}
Without it, a single buggy @types/foo package can break your entire build. With it, your code still gets full type-checking, but the upstream .d.ts files are taken at face value.
# Without skipLibCheck — slow and brittle
time npx tsc --noEmit
Output:
src/main.ts:1:10 - error TS7016: Could not find a declaration for ...
node_modules/@types/some-pkg/index.d.ts:42:10 - error TS2304: Cannot find ...
Found 3 errors in 2 files.
real 0m4.821s
# With skipLibCheck — fast and tolerant
time npx tsc --noEmit
Output:
src/main.ts:1:10 - error TS7016: Could not find a declaration for ...
Found 1 error in 1 file.
real 0m1.412s
The trade-off: if you author a library and ship .d.ts files, skipLibCheck: true means consumers won't see errors in your declarations. Always run a CI step with skipLibCheck: false before publishing.
Lib option — built-in type libraries
The lib option lists which TypeScript built-in libraries to include. It's separate from target because some runtimes (browsers, web workers, Node) expose different globals.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}
Common lib values:
| Value | Adds |
|---|---|
ES2015 … ES2024 | ECMAScript globals for that year (Promise, Map, Array.prototype.flat, etc.) |
ESNext | Latest spec features (proposals at stage 3+) |
DOM | window, document, HTMLElement, etc. |
DOM.Iterable | NodeList and HTMLCollection iteration support |
WebWorker | Worker globals (self, postMessage) |
WebWorker.Iterable | Iteration support for WebWorker globals |
ScriptHost | Windows ScriptHost APIs (legacy) |
decorators | Symbol.metadata (TS 5.2+) |
decorators.legacy | Legacy decorator metadata typings |
For a Node-only project:
{
"compilerOptions": {
"lib": ["ES2022"]
}
}
For a Cloudflare Worker:
{
"compilerOptions": {
"lib": ["ES2022", "WebWorker"],
"types": ["@cloudflare/workers-types"]
}
}
If you omit lib, TypeScript defaults to a sensible set based on target — including DOM. Browser projects don't need to set lib explicitly; Node projects should set lib: ["ES2022"] (no DOM) to avoid mistakenly using browser globals.
Common pitfalls
module: NodeNextwithout.jsextensions — every relative import must end in.jseven though sources are.ts. Add them, or switch tomoduleResolution: bundler.pathsworking in the editor but breaking at runtime — the editor usespaths; Node doesn't. Either set uptsconfig-paths/registeror mirror the alias in the bundler.extendsfrom a package not indevDependencies— CI fails on missing base config. Always declare@tsconfig/*in devDependencies.composite: truewithoutoutDirandrootDir— TS errors because composite projects must emit. Set both.skipLibCheck: falseon a large codebase — every@types/*package's bugs become your errors. Default totrue; only flip tofalseon CI before publishing.target: ES5with modern syntax —async/awaitcompiled to ES5 generates a huge polyfill helper. PickES2017or later.strict: trueplusnoImplicitAny: false— TypeScript silently re-enablesnoImplicitAnybecause it's part ofstrict. Either keepstrict: true(turning everything on) or setstrict: falseand pick individual flags.- Setting
modulewithoutmoduleResolution— defaults can surprise:module: NodeNextdefaultsmoduleResolutiontoNodeNext, butmodule: ESNextdefaults it toClassic(effectively broken). Always set both. includeandfilestogether —filestakes precedence and is exact-match. If you setfiles: ["src/main.ts"], only that one file (plus its transitive imports) compiles, regardless ofinclude.- Comments in tsconfig.json — TS 1.7+ supports JSONC (JSON with comments), but some external tools (older
prettier, some linters) reject them. Usetsconfig.jsonfor the file but author with// ...comments freely —tscis happy.
Real-world recipes
Strict tsconfig for a new project
The baseline that catches the most bugs without being unreasonable. Drop this into a new project and tune from there.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
npx tsc --noEmit
Output:
(no output — exit code 0)
Bun + TypeScript + Vite project
Bun reads tsconfig.json natively. For a Vite-based front-end built with Bun, the config looks slightly different — moduleResolution: "bundler", no emit, JSX preserved for Vite to transform.
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"noEmit": true,
"allowImportingTsExtensions": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["@types/bun", "vite/client"]
},
"include": ["src"]
}
bun add -d typescript @tsconfig/strictest @types/bun
Output:
bun add v1.2.18
installed typescript@5.4.5
installed @tsconfig/strictest@2.0.5
installed @types/bun@1.1.5
3 packages installed [127.00ms]
Cloudflare Workers tsconfig
Cloudflare Workers run on V8 isolates — no Node, no DOM, just fetch-style request handling. Use the @cloudflare/workers-types package for Worker globals.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "WebWorker"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"noEmit": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*", "worker-configuration.d.ts"]
}
npx tsc --noEmit
npx wrangler deploy
Output:
Uploaded my-worker (1.42 sec)
Published my-worker (0.32 sec)
https://my-worker.example.workers.dev
Split tsconfigs for source + tests
Tests usually need different lib/types than source — @types/jest, vitest/globals, @types/node. Split into two configs and use extends.
// tsconfig.json — source
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
// tsconfig.test.json — tests
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["vitest/globals", "@types/node"]
},
"include": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}
npx tsc --noEmit -p tsconfig.test.json
Output:
(no output — exit code 0)
The test config inherits everything from the main config, adds Vitest globals, and excludes nothing — it picks up tests that tsconfig.json skips.
Migrate a JavaScript project incrementally
Existing JS codebase, no rewrite. Use allowJs: true + checkJs: false to ship without errors; flip checkJs: true per-file with // @ts-check.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowJs": true,
"checkJs": false,
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
In any .js file, add the magic comment to opt in:
// @ts-check
/**
* @param {string} name
* @returns {string}
*/
function greet(name) {
return `Hello, ${name}!`;
}
tsc --noEmit now type-checks just this file using JSDoc annotations. See project references for per-directory migration.
Verify what your tsconfig actually does
Three commands answer "is my config doing what I think":
# 1. After extends resolution and glob expansion
npx tsc --showConfig
# 2. Every file the compiler will read
npx tsc --listFiles --noEmit | wc -l
# 3. Per-pass timing
npx tsc --diagnostics --noEmit
Output:
{ "compilerOptions": { ... }, "include": [ ... ] }
247
Files: 47
Lines: 18421
Parse time: 0.42s
Bind time: 0.12s
Check time: 1.87s
Total time: 2.41s
If the file count is way higher than expected, check include/exclude and consider tightening types. If check time dominates, look at generic-heavy hot spots.