logodev atlas
5 min read

TypeScript Configuration — tsconfig Deep Dive

Anatomy of tsconfig.json

json{
  "compilerOptions": { /* how TS compiles */ },
  "include": ["src/**/*"],         // files to include
  "exclude": ["node_modules", "dist"],
  "files": ["src/main.ts"],        // specific files (overrides include)
  "extends": "./tsconfig.base.json" // inherit from another config
}

Critical Compiler Options

Target & Module

json{
  "target": "ES2022",
  // What JS version to emit. Higher = smaller output (no polyfills for newer features).
  // ES2022 supports: top-level await, class fields, error.cause, at()
  // For Node 18+: "ES2022" or "ESNext"
  // For old browsers: "ES5" or "ES2015"

  "module": "NodeNext",
  // Module system for emitted code:
  // "CommonJS"  → require/module.exports (Node.js default pre-ESM)
  // "ESNext"    → import/export (modern bundlers)
  // "NodeNext"  → respects package.json "type" field (Node.js 12+)
  // "Preserve"  → keep as-is (TS 5.4+)

  "moduleResolution": "NodeNext",
  // How TS resolves imports:
  // "Node10"    → old Node.js behavior
  // "NodeNext"  → modern Node.js with ESM support, respects exports field
  // "Bundler"   → Vite/esbuild style (no extension required)
}

Strict Mode Options

json{
  "strict": true,
  // Shorthand for enabling ALL strict checks:

  "strictNullChecks": true,
  // null and undefined are not assignable to other types
  // let x: string = null; → ❌

  "strictFunctionTypes": true,
  // Function parameters are checked contravariantly
  // Prevents unsafe function type assignments

  "strictBindCallApply": true,
  // call/bind/apply are type-checked

  "strictPropertyInitialization": true,
  // Class properties must be initialized in constructor
  // Use ! to assert: name!: string

  "noImplicitAny": true,
  // Error on expressions inferred as any
  // function f(x) {} → x implicitly has type 'any'

  "noImplicitThis": true,
  // Error when 'this' is implicitly any

  "useUnknownInCatchVariables": true,
  // catch(e) → e is unknown, not any (TS 4.4+)
  // Forces you to type-check error before using it

  "alwaysStrict": true,
  // Emit "use strict" in all files
}

Additional Checks

json{
  "noUnusedLocals": true,
  // Error on unused local variables

  "noUnusedParameters": true,
  // Error on unused function parameters
  // Prefix with _ to suppress: _unusedParam

  "noImplicitReturns": true,
  // All code paths in a function must return a value

  "noFallthroughCasesInSwitch": true,
  // Disallow fallthrough between switch cases

  "exactOptionalPropertyTypes": true,
  // { x?: string } means x can be absent OR string, NOT undefined
  // Prevents: obj.x = undefined when x is optional (TS 4.4+)

  "noUncheckedIndexedAccess": true,
  // arr[0] returns T | undefined, not T
  // Safer but requires more null checks

  "noPropertyAccessFromIndexSignature": true,
  // Must use bracket notation for index signature access: obj['key'] not obj.key
}

Paths & Module Resolution

json{
  "baseUrl": ".",
  "paths": {
    "@/*": ["src/*"],
    "@components/*": ["src/components/*"],
    "@utils": ["src/utils/index.ts"]
  },
  // Note: paths only affect TS type resolution, not runtime
  // You also need tsconfig-paths or bundler path mapping

  "rootDir": "src",
  "outDir": "dist",

  "rootDirs": ["src", "generated"],
  // Treat multiple dirs as one virtual root

  "typeRoots": ["./node_modules/@types", "./types"],
  // Where to look for @types packages

  "types": ["node", "jest"],
  // Only include specific @types packages (omit others)
}

Source Maps & Declarations

json{
  "sourceMap": true,
  // Generate .js.map files (for debugging in original TS)

  "inlineSourceMap": false,
  // Embed source map inline in JS file (vs separate file)

  "declaration": true,
  // Generate .d.ts files (needed for libraries)

  "declarationMap": true,
  // Generate .d.ts.map for declaration files

  "declarationDir": "types",
  // Output directory for .d.ts files

  "emitDeclarationOnly": true,
  // Only emit .d.ts, not .js (when bundler handles transpilation)
}

Performance & Emit

json{
  "incremental": true,
  // Save build info to speed up subsequent compiles
  "tsBuildInfoFile": ".tsbuildinfo",

  "skipLibCheck": true,
  // Skip type checking of .d.ts in node_modules
  // Faster but misses lib type errors — almost always true in projects

  "isolatedModules": true,
  // Each file must be independently compilable
  // Required for: Babel, esbuild, SWC transpilation
  // Effect: no const enum, no namespace merging across files

  "noEmit": true,
  // Type-check only, don't emit JS (when Vite/esbuild handles build)

  "removeComments": true,
  "importHelpers": true,
  // Use tslib helpers instead of inlining (reduces bundle size)
}

ES Modules Interop

json{
  "esModuleInterop": true,
  // Allows: import React from 'react' (instead of import * as React)
  // Generates __importDefault and __importStar helpers

  "allowSyntheticDefaultImports": true,
  // Implied by esModuleInterop. Allows default imports from CJS modules.

  "allowImportingTsExtensions": true,
  // Allow: import './foo.ts' — needed with bundler moduleResolution (TS 5.0+)

  "resolveJsonModule": true,
  // import data from './data.json' — fully typed

  "verbatimModuleSyntax": true,
  // import type must be used for type-only imports
  // Ensures imports are stripped correctly by bundlers (TS 5.0+)
}

Preset Configurations

Node.js App

json{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true
  }
}

Next.js / React App

json{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "jsx": "preserve",
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "incremental": true,
    "allowJs": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "paths": { "@/*": ["./src/*"] }
  }
}

Library (publishable npm package)

json{
  "compilerOptions": {
    "target": "ES2019",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "skipLibCheck": true,
    "importHelpers": true,
    "verbatimModuleSyntax": true
  }
}

Project References (monorepo)

json// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,   // required for project references
    "declaration": true,
    "outDir": "dist"
  }
}

// packages/app/tsconfig.json
{
  "compilerOptions": { "outDir": "dist" },
  "references": [{ "path": "../core" }]
}

Build command: tsc --build (incremental, respects dependency order)


JSX Options

json{
  "jsx": "react",          // classic: React.createElement (pre-17)
  "jsx": "react-jsx",      // automatic: no React import needed (React 17+)
  "jsx": "react-jsxdev",   // automatic + debug info
  "jsx": "preserve",       // keep JSX as-is (for bundler to handle)
  "jsx": "react-native"    // preserve but for RN metro bundler
}

Common Gotchas

ts// 1. paths doesn't fix runtime resolution
// tsconfig paths: "@/utils" → "src/utils"
// Without bundler plugin: import '@/utils' fails at runtime

// 2. strict: true doesn't include noUnusedLocals / noPropertyAccessFromIndexSignature
// Those are separate options

// 3. isolatedModules breaks const enums
const enum Direction { Up, Down } // ❌ with isolatedModules

// 4. moduleResolution: "node" vs "node16" vs "bundler"
// node16/nodenext: requires file extensions in imports
import { foo } from './utils.js'; // note: .js even in TS files!

// 5. target affects which syntax is downleveled
// target: "ES5" → classes, arrows, spread all compiled down
// target: "ES2022" → native class fields, at(), etc.

// 6. lib is independent of target
// target: "ES5", lib: ["ES2022"] → can use ES2022 APIs but compiles to ES5 syntax
// Watch out: you still need polyfills for the runtime
[prev·next]