Last 30 Days
No notifications
tsconfig.json is the steering wheel of every TypeScript project. The right options catch real bugs in CI; the wrong ones leave gaping holes. Here are the flags that matter, the ones that don't, and the editor/lint setup that makes day-to-day work pleasant.
tsconfig.json{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"noEmit": true,
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
},
"include": ["src", "next-env.d.ts"],
"exclude": ["node_modules", "dist"]
}That's a solid Next.js / Vite setup. Now let's break down what each cluster does.
strict: trueSingle switch that enables the whole strict family:
| Sub-flag | What it catches |
noImplicitAny | Untyped parameters / variables |
strictNullChecks | null / undefined are no longer assignable to everything |
strictFunctionTypes | Catches unsafe function-parameter variance |
strictBindCallApply | Type-checks bind/call/apply arguments |
strictPropertyInitialization | Class fields must be initialised |
alwaysStrict | Emits "use strict" |
useUnknownInCatchVariables | catch (e) is unknown not any |
Never turn this off. If strict is too painful on a legacy codebase, ratchet by enabling sub-flags one at a time.
noUncheckedIndexedAccessOff by default. Turn it on. With it:
const a: string[] = ["x"];
const v = a[2]; // string undefined ✅ honestWithout it, v is string and you get a runtime crash. This single flag prevents an entire class of bugs.
noImplicitReturns / noFallthroughCasesInSwitch
Cheap to enable, catches real mistakes.
Module / target
Option Use it for
targetJS feature level for emit (ES2022 is fine almost everywhere)
libBuilt-in type definitions to include ("DOM" for browser code, omit for pure Node)
moduleOutput module syntax — for bundlers use ESNext
moduleResolutionHow imports resolve — Bundler for Vite/Next/Webpack, NodeNext for raw Node
esModuleInteropLets you do import express from "express" (CJS interop)
isolatedModulesRequired by single-file transpilers (esbuild/swc) — every file must be its own module
jsxpreserve for Next/Vite, react-jsx for plain TSC builds
Performance / quality of life
"skipLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
skipLibCheck is almost always on — typing errors in node_modules/*.d.ts are not yours to fix. incremental makes subsequent tsc runs much faster.
Path aliases (you're already using these)
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}
}
Now import { foo } from "@/lib/foo" resolves anywhere in the project. Bundler must respect them too — Next.js does this out of the box.
Project references (monorepos)
If you have multiple packages in one repo, set up project references:
// tsconfig.json (root)
{
"files": [],
"references": [
{ "path": "./packages/ui" },
{ "path": "./packages/server" }
]
}
Each package has its own tsconfig with "composite": true. tsc --build then compiles only what changed. Critical at scale.
Linting — eslint + typescript-eslint
ESLint catches stylistic & many bug-class issues TS doesn't.
npm i -D eslint @eslint/js typescript-eslint
Recommended config (flat config / ESLint 9+):
// eslint.config.mjs
import js from "@eslint/js";
import tseslint from "typescript-eslint";export default [
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: { project: ["./tsconfig.json"] },
},
},
];
recommendedTypeChecked is the killer feature — it uses the type checker so rules can spot, e.g., a forgotten await.
CI command
The single most valuable line you can add to CI:
tsc --noEmit
Combined with npm test and npm run lint, this is the minimum bar for shipping.
Common templates
Next.js (this project)
module: ESNext, moduleResolution: Bundler, jsx: preserve
noEmit: true (Next handles emit)
Node (server / CLI)
module: NodeNext, moduleResolution: NodeNext
target: ES2022, lib: ["ES2022"], NO "DOM"
- Emit with
tsc (noEmit: false, outDir: "dist")
Library
- Add
"declaration": true and "declarationMap": true so consumers get .d.ts + jump-to-source.
What NOT to do
- ❌ Don't ship
any to skip strictness. Use unknown and narrow.
- ❌ Don't disable rules to silence them. Either fix the code or document the exception.
- ❌ Don't set
allowJs: true permanently — it's a migration tool.
- ❌ Don't rely on
@ts-ignore. Prefer @ts-expect-error so it errors when the underlying issue is fixed.