tsconfig & Compilation
You will learn how tsconfig.json actually shapes a TypeScript project: which flags tighten type checking, how the compiler resolves module specifiers and picks an output target, how declaration files are produced and consumed, and how type-checking fits into a real build. Interviewers ask because most TypeScript setup pain — 'cannot find module', 'it compiles locally but not in CI', a published package with no types — traces straight back to a compiler option nobody on the team understood.
part ofTypeScriptoverview, primer and where to startread it →on this pageshowhide
explore
- Strictness & Correctness Flags15 questions
- The strict Family5 questions
- Correctness Flags Beyond strict5 questions
- isolatedModules & verbatimModuleSyntax5 questions
- Modules, Targets & Resolution11 questions
- target, module & lib6 questions
- moduleResolution, Interop & Path Mapping5 questions
- Declaration Files & Publishing12 questions
- Emitting & Shipping .d.ts6 questions
- Ambient Declarations & Augmentation6 questions
- JavaScript Interop & Migration10 questions
- allowJs, checkJs & JSDoc Typing5 questions
- Incremental Migration Strategy5 questions
- Build Topology & Toolchain10 questions
- Project References & Incremental Builds5 questions
- Transpile-Only Pipelines & Type-Check Gates5 questions
questions
page 2 of 2In tsconfig.json, what is the difference between the `esModuleInterop` and `allowSyntheticDefaultImports` compiler options?
basics
~20 sallowSyntheticDefaultImports is type-checking only: it lets you write a default import from a module that declares no default export. esModuleInterop additionally changes CommonJS emit to wrap the required value, and it turns allowSyntheticDefaultImports on for you.
What does the TypeScript compiler option `useDefineForClassFields` change about how class fields are emitted, what is its default, and what kind of working code breaks when it becomes true?
basics
~20 suseDefineForClassFields switches class field emit from plain assignment to Object.defineProperty semantics, matching the ECMAScript standard. It defaults to true when target is ES2022 or higher. Fields declared without an initializer then overwrite inherited values with undefined, and they shadow base-class accessors instead of calling them.
With `"target": "es5"` in tsconfig, TypeScript accepts `for (const x of myArray)` but rejects `for (const [k, v] of myMap)` and points you at the `downlevelIteration` option. Why the difference, and what does enabling that flag change in the emitted code?
basics
~20 sAt an ES5 target TypeScript compiles for...of over arrays and strings into a plain index loop, which cannot work for a Map. downlevelIteration makes the compiler emit helpers that call Symbol.iterator instead, so any iterable works — but the runtime must actually provide Symbol.iterator.
In TypeScript, what does the exactOptionalPropertyTypes compiler option change about a property declared retries?: number, and how do you keep allowing an explicit undefined?
basics
~20 sexactOptionalPropertyTypes separates absent from present-and-undefined. With it on, retries?: number means the property may be missing but must hold a real number when present, so assigning { retries: undefined } is an error; declare retries?: number | undefined to allow both.
In TypeScript, what does the noImplicitOverride compiler option require, which class members does it apply to, and what bug is the override keyword designed to catch?
basics
~20 snoImplicitOverride requires the override keyword on any member that redefines a concrete member inherited from a base class, including properties and accessors. It catches renamed or deleted base members, whose overrides would otherwise silently become brand-new members.
In TypeScript, what does the noUncheckedIndexedAccess compiler option change about the type of rows[0] for const rows: string[], and which accesses does it deliberately leave alone?
basics
~10 sWith noUncheckedIndexedAccess on, every read through an array index or an index signature gains undefined: rows[0] is string | undefined rather than string. Declared properties, fixed tuple positions and for...of iteration are unaffected.
Why does TypeScript's `isolatedModules` flag reject references to an ambient `declare const enum`, and what does a per-file transpiler do with an ordinary `const enum`?
basics
~20 sA const enum is normally compiled away by inlining its member values, which requires reading the declaring file. An ambient const enum emits no runtime object at all, so a tool that reads one file has nothing to inline and nothing to reference — hence the error.
What does TypeScript's `verbatimModuleSyntax` flag change about how imports and exports are emitted, and which older options does it replace?
basics
~10 sverbatimModuleSyntax replaces import elision with one syntactic rule: anything written with a type modifier is dropped entirely, and everything else is emitted exactly as written. Added in TypeScript 5.0, it supersedes importsNotUsedAsValues and preserveValueImports.
In a TypeScript project compiled with strict enabled, what is the static type of `e` in `try { ... } catch (e) { ... }`, and what do you have to do before reading `e.message`?
basics
~20 sUnder strict, the useUnknownInCatchVariables flag types a catch clause variable as unknown rather than any, so reading any property off it is a compile error. You must narrow first — typically with an instanceof Error check — before touching e.message.
A TypeScript class compiled with strict reports "Property 'name' has no initializer and is not definitely assigned in the constructor". Which compiler flag produces that error, and what are the legitimate ways to satisfy it?
basics
~20 sThe strictPropertyInitialization flag, enabled by strict and requiring strictNullChecks, produces that error. Satisfy it by giving the field an initializer, assigning it unconditionally in the constructor, or admitting it may be missing by making the type optional or including undefined.
A TypeScript monorepo builds with `tsc -b`, yet every CI run recompiles all projects from scratch even though only one package changed. How do you diagnose and fix that?
basics
~20 sRun tsc -b --verbose to see why each project is considered out of date. Typically CI restores neither the .tsbuildinfo files nor the emitted outputs, or a fresh checkout resets file timestamps, or a changed compiler version or config invalidates the recorded build state.
Your CI step that runs tsc --noEmit has grown to several minutes on a mid-sized TypeScript repo. Which compiler flags and measurements would you use to find where the time is actually going?
basics
~20 sStart with tsc --extendedDiagnostics for a breakdown of parse, bind, check and emit time plus file, type and instantiation counts. Then use --generateTrace with @typescript/analyze-trace to find the specific files and types that dominate checking.
A TypeScript project has a declaration file containing `declare const BUILD_ID: string;`. Every use of `BUILD_ID` type-checks, yet the deployed app throws "BUILD_ID is not defined" at runtime. What does the `declare` keyword actually guarantee, and how would you stop this class of failure?
basics
~20 sNothing. An ambient declaration is an unverified promise to the type checker; it emits no JavaScript and never checks that the thing exists. Guarantees have to come from the build or from a runtime-validated module, not from the declaration.
A TypeScript package ships `.d.ts` files and sets the `types` field in package.json, but after it adds an `exports` map, consumers on `"moduleResolution": "node16"` report that its types are gone. Why, and how do you fix it?
basics
~20 sUnder node16 resolution, an exports map takes over entry-point resolution and the top-level types field is no longer consulted. Add a types condition inside exports, listed first in each condition object, or place a matching .d.ts beside every resolved JavaScript file.
A half-migrated codebase sets "strict": true in tsconfig.json and tsc reports several thousand errors. How would you get the project to full strict without freezing feature work?
basics
~20 sDo not merge the all-at-once change. Stage it: keep strict on but switch individual flags back off and re-enable them one at a time, or scope strict to migrated directories, and make CI hold a baseline error count that is only ever allowed to fall.
A dependency imports fine at runtime, but `tsc` reports TS2307 "Cannot find module 'pkg/utils' or its corresponding type declarations" — the package exposes that subpath only through an `exports` map in its package.json, and the tsconfig sets `"moduleResolution": "node10"`. What is going on, and what do `node16`/`nodenext` and `bundler` change?
basics
~20 snode10 is the legacy resolution algorithm: it walks node_modules by directory and file name and ignores package.json exports entirely, so a subpath declared only there is invisible to the compiler. node16, nodenext and bundler read exports maps.
A TypeScript build that downlevels to ES5 emits the same helper functions — `__extends`, `__awaiter`, `__spreadArray` — at the top of many output files. What does the `importHelpers` compiler option change, what must the package declare, and when is it the wrong choice?
basics
~20 sBy default the compiler inlines its downleveling helpers into every file that needs them. importHelpers makes it import them from the tslib package instead, so they exist once. tslib then becomes a real runtime dependency, which a published library must list under dependencies.
Your team wants to adopt TypeScript's beyond-strict correctness flags — noUncheckedIndexedAccess, exactOptionalPropertyTypes and noUnusedLocals — on a large existing codebase. How would you sequence the rollout, and which flag do you expect to cost the most?
basics
~20 sMeasure each flag's error count first by enabling it alone on the command line, then adopt one flag per change. Expect noUncheckedIndexedAccess to dominate, since it touches every array index and dictionary lookup, and watch that its fixes are real checks rather than assertions.
A TypeScript package's index.ts contains `export { Config } from './config';` where Config is an interface. Built with `tsc` the package works, but built by a per-file transpiler such as esbuild or swc, consumers crash with an error saying './config' provides no export named 'Config'. Why does the same source behave differently, and how do you fix it?
basics
~20 stsc reads ./config, sees Config is an interface, and drops it from the re-export. A per-file transpiler never reads that file, so it emits a real re-export of a binding that the emitted config.js does not have. Fix it with export type, and enable isolatedModules so the compiler catches every such site.
You set "strict": true in the tsconfig.json of an established TypeScript codebase and tsc reports several thousand errors. How do you get the project onto strict without a big-bang rewrite?
basics
~20 sSplit the work by flag and by file. Enable the cheap members of the strict family first, then adopt strictNullChecks and noImplicitAny through a second tsconfig that extends the base, sets strict, and lists a growing set of already-clean files that CI checks alongside the loose build.
Your team wants request-scoped user data available as `req.user` throughout a TypeScript service by augmenting the web framework's `Request` interface globally. As the engineer setting the standard, how do you weigh that global augmentation against the alternatives?
basics
~20 sGlobal augmentation is convenient but unscoped and unverified: the property appears on every request in every file whether or not the middleware ran. Prefer it only for genuinely universal, optional data, and use distinct types or an explicit context for anything a handler must be able to rely on.
You own a large, actively developed JavaScript codebase and the team wants type safety. How would you decide between an incremental TypeScript migration, a rewrite in TypeScript, and staying on JavaScript with types checked by tsc?
basics
~20 sIncremental is the default because the codebase stays shippable and every step is revertable. A rewrite is only defensible for a small, stable, well-covered module. Staying on typed JavaScript is the right answer when the build cannot change or the code ships as source.
You own the tsconfig for a package published to npm and for the application that consumes it. How do you decide `target` and `lib` for each, and what does the TypeScript compiler explicitly not do for you?
basics
~20 sPick target from the oldest runtime that must execute the emitted code, and lib from the APIs that runtime actually provides plus any polyfills you ship. The compiler downlevels syntax only: it never polyfills built-ins, and the lib you choose leaks into your published types as a requirement on consumers.
What do the triple-slash directives `/// <reference path="..." />`, `/// <reference types="..." />` and `/// <reference lib="..." />` each do in a TypeScript file, and what makes one of them silently do nothing?
basics
~20 spath pulls another file into the compilation, types declares a dependency on a package's ambient type declarations, and lib pulls in a built-in library file such as es2015. A directive placed after any statement is treated as an ordinary comment and ignored.
After adding `"types": ["node"]` to compilerOptions in a tsconfig.json, every test file starts failing with "Cannot find name 'describe'". What does the `types` option control, and how does it differ from `typeRoots`?
basics
~20 sThe types option is an allow-list for the @types packages that are auto-included as globals. Naming any package suppresses the automatic inclusion of all the others, so the test framework's global declarations disappear. typeRoots changes which folders that automatic inclusion scans.
What does TypeScript's `maxNodeModuleJsDepth` compiler option control, why is its default 0, and what goes wrong when you raise it?
basics
~20 smaxNodeModuleJsDepth sets how many folder levels deep under node_modules TypeScript will load JavaScript files to infer types from. It defaults to 0 so dependency types come from declaration files, not from inferring over dependency source.
You lead a team whose build strips TypeScript types without checking them. How would you decide where the type-check gate runs — the editor, a pre-commit hook, or CI — and what does each placement cost?
basics
~20 sTreat the editor as feedback, a pre-commit hook as convenience, and a blocking CI job as the only real enforcement. Put the guarantee in CI, keep the other two as fast loops, and run the identical tsc --noEmit command everywhere.
You lead a monorepo where `tsc` declaration emit dominates build time. What does TypeScript's `isolatedDeclarations` option require of the code, what does it buy, and how would you decide whether to adopt it?
basics
~20 sisolatedDeclarations, added in TypeScript 5.5, makes the compiler reject exports whose declarations cannot be produced from a single file alone, forcing explicit annotations on the public surface. That guarantee lets other tools emit .d.ts files per file, in parallel, without whole-program inference.
showing 31–58 of 58