Topics
v5.0.0
On this page · sections

Compiler & TypeScript Configuration

tsconfig.json is the contract between your codebase and the TypeScript compiler. Senior engineers know which flags prevent production bugs, how module resolution affects imports, and when project references split a monorepo into fast, incremental builds.

AreaKey optionsImpact
Strictnessstrict, strictNullChecks, noImplicitAnyNull safety, no silent any
Modulesmodule, moduleResolution, pathsImport behavior, bundler vs Node
Outputtarget, lib, outDir, declarationJS version, DOM APIs, publish types
DX / CIskipLibCheck, incremental, compositeBuild speed, project references

The tsconfig.json structure

Definition

{ "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true, "esModuleInterop": true, "resolveJsonModule": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src"], "exclude": ["node_modules", "dist"] }
  • include / exclude — which files enter the compilation.
  • extends — inherit from a shared config ("extends": "@repo/tsconfig/base.json").

Interview: Next.js and Vite projects often set noEmit: true because the bundler emits JS; TypeScript only type-checks.

Strict mode

Definition

"strict": true enables a family of compiler checks that catch the most common production bugs:

FlagWhat it catches
strictNullChecksnull / undefined used where a value is required
noImplicitAnyParameters/locals inferred as any
strictFunctionTypesUnsafe function parameter contravariance
strictBindCallApplyWrong this types on bind / call / apply
strictPropertyInitializationUninitialized class fields
noImplicitThisthis with implicit any
alwaysStrictEmit "use strict" in JS output
useUnknownInCatchVariablescatch (e) is unknown, not any
{ "compilerOptions": { "strict": true } }

Enable flags individually when migrating legacy code:

{ "compilerOptions": { "strict": false, "strictNullChecks": true, "noImplicitAny": true } }

strictNullChecks and noImplicitAny

strictNullChecks forces explicit handling of null/undefined; noImplicitAny bans silent any inference.

function getLength(s: string | null) { if (s === null) return 0; return s.length; } const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value); };

Interview: strictNullChecks is the single highest-impact strict flag for frontend apps.

strictFunctionTypes and useUnknownInCatchVariables

Function contravariance

type Handler = (event: MouseEvent | KeyboardEvent) => void; const onKey: (event: KeyboardEvent) => void = (e) => { console.log(e.key); }; // ❌ strictFunctionTypes — Handler expects broader input const handler: Handler = onKey;

Catch variables as unknown

try { await fetchUser(); } catch (error: unknown) { if (error instanceof Error) { console.error(error.message); } else { console.error("Unknown error", error); } }

Interview: Treat caught values as unknown and narrow — same discipline as API boundaries.

Module resolution

moduleResolution options

ValueUse case
bundlerVite, Webpack, esbuild — allows extensionless imports
node16 / nodenextNode ESM/CJS with package.json "exports"
classicLegacy — avoid in new projects
{ "compilerOptions": { "module": "ESNext", "moduleResolution": "bundler", "allowImportingTsExtensions": true } }

paths aliases

{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@ui/*": ["packages/ui/src/*"] } } }

TypeScript resolves @/components/Button to src/components/Button. Your bundler must mirror the same aliases in vite.config.ts or next.config.js.

package.json exports (Node libraries)

{ "name": "@acme/utils", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } } }

Use "moduleResolution": "nodenext" when publishing packages consumed by Node.

target, lib, and build performance

{ "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "jsx": "react-jsx", "skipLibCheck": true, "incremental": true } }
  • target / lib — JS syntax level and built-in APIs (DOM for document, fetch).
  • skipLibCheck — skip .d.ts in node_modules (major CI speed win).
  • App projects: noEmit: true (bundler emits JS). Libraries: declaration: true.

Project references

Definition

Split a monorepo into composite projects that build independently and in dependency order:

// packages/shared/tsconfig.json { "compilerOptions": { "composite": true, "declaration": true, "outDir": "dist" } } // apps/web/tsconfig.json { "references": [{ "path": "../../packages/shared" }] }

Build with tsc -b — only rebuilds changed projects. Requires composite: true + declaration: true on referenced packages.

Interview: Project references give incremental monorepo builds.

Migrating to strict

Practical approach

  1. Enable strictNullChecks and noImplicitAny first — highest ROI.
  2. Fix errors module by module (feature folders, not random files).
  3. Use @ts-expect-error sparingly with a ticket link; don’t blanket-disable strict.
  4. Add strict: true to CI once error count is manageable.
{ "compilerOptions": { "strict": true, "skipLibCheck": true } }

Senior Interview Summary

What to say

Strict mode bundles null safety, no implicit any, and safer function types. moduleResolution: bundler fits Vite/Next; nodenext fits published Node packages. Project references enable incremental monorepo builds. skipLibCheck speeds CI without weakening your app types.

The five flags interviewers ask about most

strictNullChecks → null/undefined must be handled noImplicitAny → no silent any inference strictFunctionTypes → safer callback typing strictPropertyInitialization → class fields initialized useUnknownInCatchVariables → catch (e) is unknown

Practical rule

New project → strict: true, skipLibCheck: true, isolatedModules: true Next/Vite app → noEmit: true, moduleResolution: bundler Published library → declaration: true, nodenext resolution Monorepo → composite + project references + shared base config

Last updated on