TypeScript tsconfig Best Practices for Production Node.js Apps
Review TypeScript compiler settings for a Node.js backend, including strict checks, module resolution, indexed access, declarations, and source maps.
A Node.js TypeScript configuration should match the runtime, module format, and checks your team expects in CI. Review the example below against those requirements instead of treating it as a universal configuration.
Example Node.js Configuration
An example 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. A lower target may transform more syntax; target does not automatically supply runtime polyfills. TypeScript target reference.
"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.
What strict Enables
strict: true enables a family of checks that can grow between TypeScript releases. The following list covers several members; current versions also include strictBuiltinIteratorReturn. Check the strict reference for your compiler version:
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.
noImplicitReturns and noFallthroughCasesInSwitch
noImplicitReturns checks return paths for inconsistent return behavior; it is not limited to functions with an explicit return annotation. 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
skipLibCheck: true skips declaration-file checking and can reduce checking time at the cost of missing errors in those declarations. Decide whether that trade-off is acceptable and document the reason.
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 emits source maps. Configure the runtime or error-reporting tool to use them; emitting maps alone does not rewrite stack traces.
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.