Notifications

No notifications

/Phase 4

tsconfig & Tooling

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.

On this page

Detailed Theory

Anatomy of 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.

The "must turn on" flags

strict: true

Single switch that enables the whole strict family:

Sub-flagWhat it catches
noImplicitAnyUntyped parameters / variables
strictNullChecksnull / undefined are no longer assignable to everything
strictFunctionTypesCatches unsafe function-parameter variance
strictBindCallApplyType-checks bind/call/apply arguments
strictPropertyInitializationClass fields must be initialised
alwaysStrictEmits "use strict"
useUnknownInCatchVariablescatch (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.

noUncheckedIndexedAccess

Off by default. Turn it on. With it:

const a: string[] = ["x"];
const v = a[2]; // string 
undefined ✅ honest

Without 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

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