tsconfig.json: The Settings That Matter

A practical guide to TS compiler options interviewers ask about: strict, noImplicitAny, lib, moduleResolution, jsx, and incremental builds.

F

Frontend Interview Team

February 08, 2026

4 min read
tsconfig.json: The Settings That Matter

What you’ll learn

  • What TypeScript actually does when you run tsc
  • The few tsconfig.json options that drive most real-world behavior
  • A practical baseline config for modern frontend apps
  • Common pitfalls (that show up in interviews and production)

30‑second interview answer

tsconfig.json is TypeScript’s compiler contract: it defines the rules for type-checking (strict, noUncheckedIndexedAccess), the environment your code targets (target, lib), and how modules/types are resolved (module, moduleResolution, types). In most teams, the biggest quality lever is turning on strict mode and then tightening it with a few safety flags.


Mental model: three buckets of settings

Think of tsconfig.json as three independent concerns:

  1. Type safety – how strict TypeScript is while checking your code
  2. Runtime/emit – what JS gets produced (or not) and what runtime you target
  3. Module + type resolution – how imports, path aliases, and @types/* are found

If you’re debugging a weird TS issue, first ask: Which bucket is this in?


A solid baseline for modern frontend

This is a good starting point for Next.js / Vite / React projects. Tune from here.

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
 
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
 
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
 
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
 
    "noEmit": true,
 
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Notes:

  • noEmit: true is common in apps where the bundler (Next/Vite) does the build and TS is just for type-checking.
  • moduleResolution: "Bundler" is increasingly common with modern tooling. If you hit resolution issues, try "NodeNext".

The high-impact options (what interviewers actually care about)

strict

Enables a set of checks (including strictNullChecks, noImplicitAny, etc.).

Why it matters: strict mode prevents the “looks typed but actually isn’t” failure mode.

Interview stance:

“Strict mode reduces the bug surface area and makes refactors safe. I’d rather fix types once than ship runtime bugs repeatedly.”


strictNullChecks

Forces you to model null / undefined explicitly.

Common interview gotcha:

const el = document.getElementById('root');
// strictNullChecks makes el: HTMLElement | null
el.innerHTML = 'hi'; // error: possibly null

Correct patterns:

  • guard: if (!el) return;
  • or assert when you really know: el! (but sparingly)

noImplicitAny

Stops TypeScript from silently defaulting to any.

Why it matters: any spreads and disables checking like a virus.


noUncheckedIndexedAccess

Makes indexed access safer:

const map: Record<string, number> = {};
map['x'].toFixed(2); // becomes error (value might be undefined)

Tradeoff: adds some friction, but catches real production bugs.


exactOptionalPropertyTypes

Changes how optional props behave:

type A = { x?: string };
 
const a: A = { x: undefined }; // may error with exactOptionalPropertyTypes

Why it matters: prevents confusing “optional vs explicitly undefined” bugs.


Frontend-specific options

jsx

Common values:

  • react-jsx (modern React, automatic runtime)
  • preserve (let the framework handle JSX transform)

If you use Next.js, it typically manages this.


lib

Defines the global types you have access to (DOM, ES2022, etc.).

Symptoms of wrong lib:

  • TS can’t find fetch, Promise, URL, or DOM APIs

target

Defines the JS syntax output target. In frontend apps, this is often less critical because the bundler transpiles, but it can affect:

  • emitted syntax (if you do emit)
  • what TS assumes exists natively

Module and type resolution (most confusing in practice)

module + moduleResolution

This pair controls how imports behave.

  • module: "ESNext" is common for modern bundlers
  • moduleResolution: "Bundler" or "NodeNext" are common choices

If you see errors around .d.ts, exports fields, or ESM/CJS mismatches, it’s usually here.


baseUrl + paths

For path aliases like @/components/Button.

Important: your bundler must also understand the alias (e.g., Next config / Vite config). TS-only aliases can type-check but fail at runtime.


types / typeRoots

Controls which global @types/* packages are included.

Use these when:

  • a global type is polluting the project
  • you need to force-include a type package

Performance / monorepo options

incremental

Speeds up repeated type-checks by caching build info.

In monorepos, also look at:

  • project references (composite, references)
  • splitting configs (a base config + per-package configs)

Common mistakes (real-world + interview)

  • Turning off strictNullChecks to “fix” errors (it hides bugs, doesn’t solve them)
  • Using paths aliases in TS but not in the bundler (runtime failures)
  • Relying on skipLibCheck to hide broken local types (it should only skip external libs)
  • Mixing ESM/CJS settings without understanding package type + exports

Interview questions you should be ready for

  1. What does strict do?
  2. Why is strictNullChecks important in React?
  3. When would you use noEmit?
  4. What’s the difference between moduleResolution: Bundler and NodeNext?
  5. How do you set up path aliases safely?

Quick recap

  • strict is the biggest win.
  • lib, jsx, and module resolution determine most frontend friction.
  • Keep aliases consistent across TS + bundler.
  • Prefer fixing types over weakening checks.