TypeScript tsconfig Best Practices for Production Node.js Apps
The exact tsconfig.json settings that matter for production Node.js TypeScript apps — what to enable, what to skip, and why each flag exists.
Most teams copy a tsconfig.json from a starter template and never revisit it. That works until you hit a subtle runtime bug that TypeScript should have caught — and would have, if the right flag had been set.
The TypeScript config is not boilerplate. It is a calibration of how much the compiler will help you. Here is what a production Node.js tsconfig.json should look like and why each decision matters.
TypeScript tsconfig Best Practices for Node.js: The Full Config
A production-grade configuration for a Node.js 22+ backend:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"skipLibCheck": false,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Walk through the decisions:
Target and Module
"target": "ES2022" tells TypeScript which JavaScript features to emit. Node 22 supports ES2022 natively, so there is no need to downcompile modern syntax. Using a target older than your runtime adds dead code from polyfills.
"module": "NodeNext" with "moduleResolution": "NodeNext" makes TypeScript handle ESM and CJS correctly for Node.js, including requiring file extensions in imports. This is a breaking change from older configs but it is correct for modern Node.
strict: true and What It Actually Enables
strict: true is a shorthand for eight flags:
strictNullChecks—nullandundefinedare not assignable to other typesnoImplicitAny— TypeScript will not silently inferanystrictFunctionTypes— function parameter types are checked contravariantlystrictBindCallApply—bind,call, andapplyare type-checkedstrictPropertyInitialization— class properties must be initialized in the constructornoImplicitThis—thisin functions must have an explicit typealwaysStrict— emits"use strict"in every output fileuseUnknownInCatchVariables— catch clause variables default tounknowninstead ofany
If you are on a greenfield project, enable this from day one. If you are migrating, enable it after the bulk of files are .ts.
noUncheckedIndexedAccess: The Flag strict Misses
This one is not in strict: true and it catches a very common class of runtime crash. Without it, arr[0] has type T even though the array could be empty. With it, arr[0] has type T | undefined, forcing you to handle the empty case.
const ids: string[] = getIds();
const first = ids[0];
// Without flag: first is string — crash if array is empty
// With flag: first is string | undefined — compiler forces a checkEnable it. Fix the errors by adding null coalescing (??) or explicit guards. It is one of the highest-ROI flags available.
noImplicitReturns and noFallthroughCasesInSwitch
noImplicitReturns ensures every branch of a function that declares a return type actually returns. This catches the "function falls off the end and returns undefined silently" bug.
noFallthroughCasesInSwitch catches switch cases that fall through to the next case without a break or return. This is almost always a bug.
exactOptionalPropertyTypes
Without this flag, { age?: number } lets you write obj.age = undefined even though that is semantically different from omitting the property. With it enabled, setting an optional property to undefined is a type error — you must omit the key instead. This matters most when you are serializing to JSON or passing objects to strict APIs.
skipLibCheck
The safe default is false. With skipLibCheck: true, TypeScript skips type-checking .d.ts files in node_modules. This speeds up compilation but lets errors in third-party types silently pass. Only flip this to true if a dependency ships broken types and you have no other option. Document why.
declaration and sourceMap
declaration: true emits .d.ts files alongside your output. This is required if you are publishing a package that other TypeScript projects consume. declarationMap: true adds source maps for those declaration files so IDE "go to definition" navigates to your .ts source rather than the compiled .d.ts.
sourceMap: true maps runtime error stack traces back to your source files. Without it, a production crash points to minified compiled JavaScript, not the line you actually wrote.
Running Type Checks in CI
Your build step should not be your type check. Keep them separate:
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc"
}
}Run typecheck on every pull request. Run build to produce artifacts. --noEmit is faster because it skips writing files. If typecheck is slow on a large codebase, look at project references to enable incremental checking across packages.
Getting the configuration right is the foundation. If you are building a TypeScript backend and want it done correctly from the start, talk to Clixo.