In a TypeScript project, what does the `extends` field in tsconfig.json do, and relative to which directory are relative paths written inside the inherited base config resolved?
answer
- inherit settings, override per key
- paths belong to the file they sit in
- arrays replace, they do not merge
- location options stay in the leaf config
- tsc --showConfig prints the merged result
basics
~20 sThe extends field makes a tsconfig.json inherit another config file's settings, which the local file can then override key by key. Relative paths written in the base file resolve against the base file's own directory, not the inheriting project's.
solid answer
~40 s`extends` points at another tsconfig.json — a sibling file, a path like `../tsconfig.base.json`, or a config shipped inside an npm package — and loads it first; the inheriting file's own settings are then applied on top, overriding per key inside `compilerOptions`. The catch is path resolution: every relative path in a config resolves relative to the file it was **written in**, so an `outDir: "./dist"` sitting in the shared base points every package at the base's directory, not at each package's own folder. That is why shared bases usually carry only location-independent options — `strict`, `target`, `module`, `lib` — and leave `outDir`, `rootDir`, `include` and `references` to each project. `files`, `include` and `exclude` are replaced wholesale by the inheriting file rather than merged.
code
json · 11 lines{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"declaration": true,
"declarationMap": true,
"composite": true
}
}go deeper
Know that extends pulls in another tsconfig's settings and that your own file overrides them key by key. Be able to name why the shared base holds strict and target but not outDir.
Explain the resolution rule precisely — paths resolve against the file they are written in — and that files, include and exclude are replaced rather than merged. Mention tsc --showConfig as the way to verify.
Show you have designed the layout: what belongs in the shared base, what must stay per package, and how a wrong inherited outDir shows up as packages overwriting each other's output. Know ${configDir} as the deliberate escape hatch.
Own the tradeoff between one permissive base everyone extends and several stricter bases per package tier, including how you roll out a new correctness flag across dozens of packages without a flag day.
## What `extends` is for A TypeScript codebase split into several packages usually wants one set of compiler rules everywhere — same `strict` settings, same `target`, same `lib` — with each package differing only in where its sources and outputs live. `extends` is the mechanism for that: a tsconfig.json names a base configuration file, the compiler loads the base first, and then applies the inheriting file's own entries on top. ```json { "extends": "../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` The value can be a relative path to a file, a path to a directory containing a `tsconfig.json`, or a package-style specifier such as `"@acme/tsconfig/base.json"`, which is looked up through node module resolution — that is how published config packages work. ## How merging actually works Merging is per key, not deep. Inside `compilerOptions`, any option the inheriting file sets wins for that option; every option it does not mention keeps the base's value. Array-valued options are replaced, not concatenated: a local `"lib": ["ES2022"]` discards the base's `lib` list entirely rather than adding to it. The file-set fields behave the same way at the top level. If the inheriting file specifies `files`, `include`, or `exclude`, its list replaces the base's list outright. Circular `extends` chains are rejected by the compiler. Since TypeScript 5.0, `extends` also accepts an array of base configs, applied left to right so later entries win: ```json { "extends": ["@acme/tsconfig/base.json", "@acme/tsconfig/strict.json"] } ``` ## The path-resolution rule and the trap it sets Every relative path found in a configuration file is resolved relative to the configuration file it originated in. This is the rule people get wrong, because the inherited options *feel* like they were written locally. Concretely: put `"outDir": "./dist"` in `packages/tsconfig.base.json`, and every package extending it emits into `packages/dist`, all on top of each other — not into `packages/foo/dist`. The same applies to `rootDir`, `baseUrl`, `typeRoots`, `include` globs, and the `path` entries under `references`. The practical consequence is a division of labour. The shared base holds only options that carry no location: `strict` and the rest of the correctness flags, `target`, `module`, `moduleResolution`, `lib`, `declaration`, `declarationMap`, `composite`. Each package's own tsconfig.json holds the location-bearing ones: `outDir`, `rootDir`, `include`, `files`, and its `references` list. TypeScript 5.5 added a way to opt out of the rule where you want it: the `${configDir}` template variable inside a path makes it resolve against the directory of the config that is *doing the extending*, so a base can legitimately say `"outDir": "${configDir}/dist"` and have every consumer emit locally. ## What `extends` does not do It does not create any build relationship. Extending a base config gives you shared *settings*; it does not tell the compiler that one project depends on another, does not affect build order, and does not let one project see another's types. That job belongs to the `references` array and `composite`, which are separate machinery — a package can extend a base without referencing anything, and can reference other projects without extending anything. It also does not change how the config is discovered. `tsc` still picks up the nearest tsconfig.json, or the one you pass to `-p`/`--project`; `extends` only changes what that file's contents resolve to. ## Checking what you actually got Because inheritance is invisible in the file you are reading, verify rather than assume. `tsc --showConfig` prints the fully resolved configuration — all inherited options merged, all relative paths already resolved to their final form. Running it in a package that misbehaves usually makes a wrong inherited `outDir` or a swallowed `include` obvious within seconds.
- How would you confirm which options a package actually ended up with after two levels of extends?Run `tsc --showConfig` in that package (or `tsc -p ./tsconfig.json --showConfig`). It prints the fully merged configuration with every inherited option included and every relative path already resolved, so you can see the effective `outDir`, `include` and `lib` rather than inferring them from three files.
- Your base config sets `"include": ["src"]` and a package adds `"include": ["test"]`. Which files get compiled?Only `test`, resolved relative to the package. `include` is not merged — the inheriting file's array replaces the base's entirely. If you want both, the package must spell out `["src", "test"]` itself.
- Does extending a shared base config make one package depend on another for build purposes?No. `extends` shares settings only. Build ordering and cross-project type visibility come from `composite` plus the `references` array; the two features are independent, and a package commonly uses both for different reasons.
A base tsconfig is like a house style guide: every document inherits it, but any address written in the style guide is the style guide's own address, not yours.
saying these in an interview costs you the question
- Thinks relative paths resolve against the inheriting project
- Expects lib or include arrays to merge with the base
- Believes extends creates a build dependency between packages
- Puts outDir and rootDir in the shared base config
- Assumes extends only accepts a relative file path