skip to content

Declaration Files & Publishing

You will learn how .d.ts files describe types without shipping implementation: how the compiler emits them, how you hand-write ambient declarations for untyped code, and how packages deliver types to their consumers. Interviewers ask because typing a third-party or untyped module is a real, recurring task rather than a trivia question.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

12

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

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

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?

level: seniorimportance: should knowfreq 45%

basics

~20 s

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

open as a page

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?

level: seniorimportance: should knowfreq 44%

basics

~20 s

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

open as a page

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?

level: principalimportance: should knowfreq 32%

basics

~20 s

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

open as a page

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?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

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

open as a page

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?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

isolatedDeclarations, 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.

open as a page