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?
answer
- compilation throws the type layer away
- the .js remembers no shapes
- a type-only mirror of each module
- one tsconfig flag, no runtime change
- declaration: true emits .d.ts
basics
~20 sThe 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 sTypeScript 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 linesexport interface Options {
apiKey: string;
retries?: number;
}
export function createClient(opts: Options) {
return {
send: (body: string): number => body.length + opts.apiKey.length,
};
}go deeper
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.
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.
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.
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