skip to content

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 pageshow

explore

questions

page 1 of 2

In a TypeScript library's tsconfig.json, what does the `declaration` compiler option emit, and why does a package that already ships compiled JavaScript still need that output?

level: juniorimportance: must knowfreq 70%

answer

  1. compilation throws the type layer away
  2. the .js remembers no shapes
  3. a type-only mirror of each module
  4. one tsconfig flag, no runtime change
  5. declaration: true emits .d.ts

basics

~20 s

The declaration option makes tsc emit .d.ts files next to the JavaScript. Compilation erases types, so the .js carries none; the .d.ts is the type-only mirror that gives consumers of the published package autocomplete and type checking.

solid answer

~50 s

TypeScript compiles by **erasing** the type layer: `interface`s vanish, annotations are stripped, and the emitted `.js` is plain JavaScript with no record of any shape. Setting `"declaration": true` tells `tsc` to also emit a `.d.ts` file per module — declarations only, no function bodies — that describes the public types of everything the module exports. That file is what a consumer's compiler reads when they import your package, and `package.json` points at it (the `types` field, or a `types` condition in `exports`). Without it, a consumer sees the import as `any` — or an outright error under `noImplicitAny` — and loses autocomplete and checking entirely. Enabling `declaration` does not change the emitted JavaScript at all; it only adds files. If another tool emits your JS, `emitDeclarationOnly` makes `tsc` produce the `.d.ts` files and nothing else.

code

typescript · 10 lines
typescript
export interface Options {
  apiKey: string;
  retries?: number;
}

export function createClient(opts: Options) {
  return {
    send: (body: string): number => body.length + opts.apiKey.length,
  };
}

go deeper

for a junior

Be ready to say plainly that compiling erases types, so a published package needs .d.ts files, and that the declaration option in tsconfig is what produces them.

for a middle

Explain that a declaration file lists exported names with inferred types and no bodies, know how outDir, declarationDir and emitDeclarationOnly place the output, and describe what a consumer sees when the files are missing.

for a senior

Show you verify the shipped artifact rather than the local build: check that declarations are inside the packed tarball and that the entry point resolves, and be able to diagnose a declaration-emit error about a type that cannot be named.

for a principal

Own the tradeoff that declaration emit requires full type inference and is usually the slowest step of a library build, and be able to argue when to keep tsc as the sole emitter versus splitting runtime output to a transpiler and using tsc for types only.

## The one fact this rests on: types are erased TypeScript is a compile-time layer over JavaScript. When `tsc` compiles a file, it type-checks it and then emits JavaScript with the type layer **removed**. An `interface` emits nothing at all. A parameter annotation emits nothing. A generic parameter emits nothing. What remains is the runtime code you would have written by hand. That is exactly what you want at runtime — zero cost — but it creates a distribution problem. If you publish only the emitted `.js`, everything the compiler knew about your API is gone. A consumer importing your package is importing plain JavaScript, and their compiler has no idea that `createClient` takes an options object with a required `apiKey: string`. ## What a declaration file is A `.d.ts` file is the type-only mirror of a module: the same exported names, with their types, and **no implementations**. Given this source: ```ts export interface Options { apiKey: string; retries?: number } export function createClient(opts: Options) { return { send: (body: string) => body.length }; } ``` the compiler emits a `.js` with the `interface` gone and the function body intact, plus a `.d.ts` like: ```ts export interface Options { apiKey: string; retries?: number } export declare function createClient(opts: Options): { send: (body: string) => number; }; ``` Note that the return type, which you never wrote, has been **inferred and written out**. Declaration emit is a projection of what the checker inferred, not a copy of what you typed. ## Turning it on ```json { "compilerOptions": { "declaration": true, "outDir": "dist" } } ``` By default the `.d.ts` lands beside its `.js` inside `outDir`. `declarationDir` moves declarations to a separate tree if you want them apart. `emitDeclarationOnly` flips the emit around: `tsc` writes the declarations and skips the JavaScript, which is how you use it when a faster transpiler produces the runtime output and `tsc` is kept only for checking and types. Once the files exist, the package has to advertise them. Historically that is the top-level `types` field in `package.json` pointing at the entry declaration; modern packages with an `exports` map declare a `types` condition per entry point instead. ## What a consumer sees when it is missing With `noImplicitAny` on — which `strict` turns on — importing a package with no declarations is an error: *"Could not find a declaration file for module ..."*. With `noImplicitAny` off, it is worse in practice: the import silently becomes `any`, so every call through it is unchecked and nobody notices. A third path is the community `@types` ecosystem, where declarations for an untyped library are published separately. ## The gotcha: not every type can be declared Because declaration emit has to *write down* inferred types, it can fail where normal compilation succeeds. The classic error is a variant of *"has or is using name 'X' from external module ... but cannot be named"*: your exported function returns a type that lives in a module you never exported or imported by name, so the compiler has no way to spell it in the `.d.ts`. The fix is to import and re-export the type, or to annotate the export explicitly. The related cost is speed. To emit declarations, the compiler must fully check and infer — you cannot produce a correct `.d.ts` by looking at one file in isolation the way a transpiler produces `.js`. That is why declaration emit is usually the slow part of a library build, and why later TypeScript versions added an option that forces exported declarations to be explicitly annotated so the emit can be done per-file by other tools. ## Practical checklist for a published package - `"declaration": true` (plus `declarationMap` if you want go-to-definition to land in your source). - The `.d.ts` files are actually inside the published files — a `files` list or `.npmignore` that excludes them silently ships an untyped package. - `package.json` names the entry declaration so the consumer's resolver finds it. - Verify by installing the packed tarball into a scratch project rather than trusting the local build.

  • Does enabling `declaration` change the JavaScript that gets emitted?
    No. It is purely additive: the same `.js` is produced, and `.d.ts` files appear alongside it. The type layer is still erased from the runtime output, so there is no bundle-size or performance cost to shipping declarations — only extra files in the package.
  • Where do the declaration files land when `outDir` is set, and how would you send them somewhere else?
    By default each `.d.ts` is written beside its `.js` inside `outDir`, mirroring the source folder structure. `declarationDir` redirects them to a separate root, which is useful when a bundler owns `dist` and you want types in their own tree. Use `emitDeclarationOnly` when `tsc` should produce declarations and no JavaScript at all.
  • Why can declaration emit fail with an error when a normal compile of the same file succeeds?
    Emitting a `.d.ts` requires the compiler to write inferred types down as text. If an exported value's type refers to something the compiler cannot name from the output file — a type from a module that is never imported or exported by name — it reports that the name "cannot be named" and refuses. Annotating the export explicitly, or re-exporting the type, resolves it.

saying these in an interview costs you the question

  • Thinks the emitted JavaScript still carries type information
  • Says interfaces exist at runtime, so declarations are unnecessary
  • Confuses .d.ts files with JavaScript source maps
  • Believes tsc emits declarations by default without the flag
  • Claims shipping declarations makes the package slower at runtime

context

open as a page

In a tsconfig.json, what does the TypeScript compiler option `allowJs` do on its own, and what changes when you also turn on `checkJs`?

level: juniorimportance: must knowfreq 58%

basics

~20 s

allowJs admits .js and .jsx files into the TypeScript program so they are resolved, inferred from and emitted — but no errors are reported in them. checkJs additionally turns the type checker on for those JavaScript files.

open as a page

In TypeScript, when you write `import _ from 'lodash'`, where does the compiler look for that package's type declarations, and what does the error "Could not find a declaration file for module 'lodash'" mean?

level: juniorimportance: must knowfreq 62%

basics

~20 s

The compiler looks inside the package first — package.json's types or typings field, or a types condition in its exports map — then falls back to node_modules/@types/lodash. If neither exists, the import carries no type information.

open as a page

In a tsconfig.json, what does the `target` compiler option control and what does the `module` option control, and why can you not use one in place of the other?

level: juniorimportance: must knowfreq 66%

basics

~20 s

In tsconfig, target sets the language level of the emitted JavaScript syntax, deciding whether things like async/await or optional chaining get rewritten. module sets the output module format, such as CommonJS require and exports or untouched ESM. The two are independent.

open as a page

In TypeScript, what does `import type { User } from './models'` mean, and how does it differ from a plain `import { User } from './models'`?

level: juniorimportance: must knowfreq 68%

basics

~20 s

import type declares the binding as types only: TypeScript always erases that statement from the emitted JavaScript, and the name cannot be used as a value. A plain import is erased only when the compiler decides nothing in the file uses it at runtime.

open as a page

With TypeScript's noImplicitAny flag enabled, why does `function greet(name) {}` fail to compile while the `u` in `users.map(u => u.id)` needs no annotation at all?

level: juniorimportance: must knowfreq 70%

basics

~20 s

noImplicitAny only errors where the compiler has no type to infer. A standalone parameter has no source of type information, so it would silently become any; a callback parameter is contextually typed from the signature it is passed to, so its type is inferred rather than implicit.

open as a page

In a tsconfig.json, what does setting `"composite": true` turn on, and what does the compiler then require of that project?

level: middleimportance: must knowfreq 58%

basics

~20 s

composite: true marks a tsconfig.json as a buildable unit that other projects can reference. It forces declaration on, enables incremental build info, defaults rootDir to the config's directory, and requires every input file to be matched by files or include.

open as a page

What does running `tsc --build` (`tsc -b`) do that plain `tsc -p tsconfig.json` does not?

level: middleimportance: must knowfreq 52%

basics

~20 s

Build mode walks the references graph: it builds each referenced project first, in dependency order, and skips any project whose outputs are already newer than its inputs. Plain tsc -p compiles only the one project and assumes its dependencies are already built.

open as a page

A project bundles its TypeScript with esbuild, and the build succeeds and ships even though the source contains type errors. Why does esbuild not fail on them, and what command do teams run to catch them instead?

level: middleimportance: must knowfreq 70%

basics

~20 s

esbuild only strips type annotations file by file; it never runs TypeScript's type checker, so type errors cannot fail it. Teams add a separate step running tsc --noEmit, which type-checks the whole program and writes no output.

open as a page

In TypeScript, a file `globals.d.ts` containing only `interface Window { analytics: Analytics }` successfully adds `window.analytics` across the project. After you add `import type { Analytics } from './analytics'` to the top of that same file, every use of `window.analytics` starts failing. What changed, and how do you fix it?

level: middleimportance: must knowfreq 70%

basics

~20 s

The top-level import turned that declaration file from a global script into a module, so its interface no longer merges with the global Window. Wrap the declaration in declare global inside the module to reach the global scope again.

open as a page

In TypeScript, how do you add a property to an interface exported by a third-party package's type declarations — say adding `user` to a framework's `Request` interface — without forking or copying that package's types, and what does the compiler require of the file you write it in?

level: middleimportance: must knowfreq 60%

basics

~10 s

Use module augmentation: in a file that is already a module, write declare module 'the-package' and re-open the exported interface with your extra members. Interface merging adds them everywhere the package's types are used.

open as a page

In a TypeScript project, importing an npm package that ships only JavaScript fails with "Could not find a declaration file for module ...". What is an `@types/*` package, how does the compiler find one, and what problems does that split introduce?

level: middleimportance: must knowfreq 66%

basics

~20 s

An @types/* package contains only declaration files for a library that ships none, published from the community DefinitelyTyped repository. TypeScript looks for it in node_modules/@types. Because it is versioned and maintained separately from the library, it can drift out of sync.

open as a page

In TypeScript, what is the difference between the `// @ts-expect-error` and `// @ts-ignore` comments, and why is one usually preferred?

level: middleimportance: must knowfreq 62%

basics

~20 s

Both suppress type errors on the line that follows them. // @ts-expect-error additionally requires an error to be there — if the line becomes valid, the compiler reports the directive as unused, so stale suppressions surface instead of rotting.

open as a page

In a JavaScript file that TypeScript is checking, how do you annotate a function's parameters and return type, declare a reusable named object type, and make a function generic — using only JSDoc?

level: middleimportance: must knowfreq 54%

basics

~20 s

Use JSDoc tags in comments: @param and @returns for a function's signature, @type for a variable or a cast, @typedef to declare a reusable named type, and @template to introduce a type parameter. TypeScript reads them as real types.

open as a page

A tsconfig.json sets `"paths": { "@app/*": ["src/*"] }` and `tsc` compiles with no errors, but running the emitted output with `node dist/index.js` fails with "Cannot find module '@app/utils'". Why does the compiler accept a specifier the runtime rejects?

level: middleimportance: must knowfreq 68%

basics

~20 s

Because paths is a type-checker-only mapping. The compiler uses it to find declarations for the specifier, but it never rewrites import specifiers in the emitted JavaScript, so the output still says '@app/utils' and the runtime has no idea what that means.

open as a page

A tsconfig sets `"target": "es5"` and `"lib": ["ES2020", "DOM"]`. A call to `[1, [2]].flat()` type-checks cleanly but throws `TypeError: ...flat is not a function` in an old browser. Explain what target and lib each do, and how you would fix this.

level: middleimportance: must knowfreq 72%

basics

~20 s

target controls emitted syntax; lib only tells the checker which built-in APIs it should believe exist. TypeScript never emits polyfills, so declaring ES2020 libs while running on an older engine type-checks calls like Array.prototype.flat that then crash at runtime.

open as a page

What does TypeScript's `isolatedModules` compiler flag do, and does enabling it change the JavaScript that `tsc` emits?

level: middleimportance: must knowfreq 57%

basics

~20 s

isolatedModules changes no output at all. It is a checking-only flag: it makes the compiler report source constructs that cannot be compiled correctly by a tool that sees one file at a time, so the code stays portable to per-file transpilers.

open as a page

In a TypeScript project's tsconfig.json, what does setting "strict": true actually do, and how would you keep it on while turning off just one of the checks it implies?

level: middleimportance: must knowfreq 78%

basics

~20 s

The strict option in tsconfig.json is not one check: it is a shorthand that switches on a whole family of individual flags, including noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables and alwaysStrict. Setting any member explicitly overrides the umbrella.

open as a page

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?

level: juniorimportance: should knowfreq 55%

basics

~20 s

The 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.

open as a page

Running a script with tsx succeeds, but running the same file with ts-node fails with a type error before anything executes. What is different about how the two tools run a TypeScript file?

level: juniorimportance: should knowfreq 52%

basics

~20 s

ts-node type-checks with the real TypeScript compiler before running, so a type error stops it. tsx uses esbuild to strip types with no checking, so it runs whatever parses. ts-node's --transpileOnly flag makes it behave like tsx.

open as a page

In a TypeScript project, `import styles from './Button.module.css'` fails with "Cannot find module './Button.module.css' or its corresponding type declarations." How do you make the compiler accept that import, and does your fix change anything at runtime?

level: juniorimportance: should knowfreq 55%

basics

~20 s

Add a wildcard ambient module declaration in a .d.ts file the project includes: declare module '*.module.css' with a default export of a string-to-string map. That only satisfies the type checker — a bundler or loader must still make the import work at runtime.

open as a page

In a project already compiled by tsc with allowJs enabled, you rename utils.js to utils.ts and change nothing inside the file. Does the JavaScript that ships behave any differently, and what does change?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Behaviour does not change: TypeScript erases types, and a freshly renamed file has none to erase, so tsc emits the same JavaScript. What changes is checking — the file is now checked as TypeScript, so implicit anys and unsound patterns start appearing as errors.

open as a page

In a TypeScript tsconfig.json, does setting "strict": true turn on every correctness check the compiler offers? Name checks it leaves off and explain why they are separate options.

level: juniorimportance: should knowfreq 48%

basics

~10 s

No. strict switches on one fixed family of type-soundness options; a separate set of correctness checks stays off until you list each one by name, including noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noImplicitReturns, noFallthroughCasesInSwitch and noUnusedLocals.

open as a page

In a TypeScript repo using project references, go-to-definition on a symbol from another project lands in a generated `.d.ts` file instead of the original `.ts` source. Which compiler option addresses this, and how?

level: middleimportance: should knowfreq 42%

basics

~10 s

Enable declarationMap in the referenced project. It emits a .d.ts.map alongside each declaration file, mapping every declaration back to the source that produced it, so go-to-definition and rename follow through to the original .ts.

open as a page

Node can run a .ts file by stripping its type syntax rather than compiling it. Which TypeScript constructs cannot be handled by stripping alone, and which tsconfig flag makes the compiler report them?

level: middleimportance: should knowfreq 40%

basics

~10 s

Constructs that emit runtime code cannot simply be erased: enum, namespaces containing runtime members, constructor parameter properties, and import x = require(...). The tsconfig flag erasableSyntaxOnly makes the TypeScript compiler report exactly those.

open as a page

What does TypeScript's `declarationMap` compiler option add on top of `declaration`, and what must a published package contain for it to work in a consumer's editor?

level: middleimportance: should knowfreq 38%

basics

~20 s

declarationMap emits a .d.ts.map next to each .d.ts, mapping declarations back to the original TypeScript. Go-to-definition then lands in real source instead of the generated declaration, but only if the package ships the maps and the .ts files they point at.

open as a page

What does TypeScript's `skipLibCheck` compiler option actually skip, and what real problems can it hide?

level: middleimportance: should knowfreq 52%

basics

~20 s

skipLibCheck stops the compiler reporting errors found inside declaration files — all of them, including your own, not just node_modules. Your code is still checked against those declarations. It mainly hides broken or mutually conflicting library types.

open as a page

What do the `// @ts-check` and `// @ts-nocheck` comments do when placed at the top of a file, and how do they interact with TypeScript's `checkJs` compiler option?

level: middleimportance: should knowfreq 46%

basics

~20 s

Placed at the top of a file, // @ts-check makes TypeScript type-check that one file even when checkJs is off, and // @ts-nocheck makes it skip that file even when checking is on. Both override the project setting per file.

open as a page

When migrating a JavaScript codebase to TypeScript file by file with tsc, why is it usually better to convert leaf modules — the ones that import little or nothing else from the project — before converting the hub modules that most of the codebase imports?

level: middleimportance: should knowfreq 50%

basics

~20 s

Because a converted file's types are only as good as its dependencies. Converting leaves first means every new TypeScript file rests on already-typed code, so inference produces real types instead of weak ones you must revisit, and each change stays small and revertable.

open as a page

During a TypeScript migration, tsc reports errors that originate inside .d.ts files under node_modules. What does setting skipLibCheck to true in tsconfig.json actually skip, and what do you give up by leaving it on?

level: middleimportance: should knowfreq 45%

basics

~20 s

skipLibCheck stops tsc type-checking the contents of every declaration file — dependencies' and your own alike, including hand-written shims. Your code is still checked against those declarations, so what you give up is any warning that a declaration file is itself wrong or inconsistent.

open as a page

showing 1–30 of 58