# 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.

- **Published:** 2026-01-06
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** typescript, nodejs, tsconfig, configuration, production
- **Canonical URL:** https://clixo.sh/blog/typescript-tsconfig-best-practices-nodejs-production

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:

```json
{
  "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` — `null` and `undefined` are not assignable to other types
- `noImplicitAny` — TypeScript will not silently infer `any`
- `strictFunctionTypes` — function parameter types are checked contravariantly
- `strictBindCallApply` — `bind`, `call`, and `apply` are type-checked
- `strictPropertyInitialization` — class properties must be initialized in the constructor
- `noImplicitThis` — `this` in functions must have an explicit type
- `alwaysStrict` — emits `"use strict"` in every output file
- `useUnknownInCatchVariables` — catch clause variables default to `unknown` instead of `any`

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.

```typescript
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 check
```

Enable 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:

```json
{
  "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.

```mermaid
flowchart LR
  PR[Pull Request] --> TC["typecheck\ntsc --noEmit"]
  TC --> Pass{Pass?}
  Pass -- No --> Fix[Fix type errors]
  Fix --> TC
  Pass -- Yes --> Merge[Merge to main]
  Merge --> Build["build\ntsc emit"]
  Build --> Deploy[Deploy artifacts]
```

---

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](https://clixo.sh/#contact).

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
