---
title: 'Declaration Files'
source: 'https://academia.sh/en/courses/typescript/declaration-files'
course: TypeScript
language: en
updated: '2026-08-17T18:09:55+00:00'
license: 'CC BY-SA 4.0'
---

# Declaration Files

Carrying type definitions separately from implementation; generating declarations from source, ambient module declarations, global scope declarations, and extending outside types with merging.

The previous lesson made it possible to check the project's own files. Code coming
from outside — a library with no type information, the global objects the runtime
provides — is still out of scope.

A **declaration file** carries only type information and produces no code. Its
extension is `.d.ts`. It is used in two directions: exposing your own code's types,
and bringing in outside code's types.

## Generating a Declaration From Source

A library package publishes its implementation as JavaScript. For TypeScript-using
consumers to have access to type information, declaration files have to be published
too. The compiler generates these from source:

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "declaration": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
```

Let there be a `package.json` at the project root with content
`{ "name": "measurement-library", "type": "module", "version": "1.0.0" }`, and let
`src/measurement.ts` be this:

```typescript
export type Unit = "C" | "Pa" | "%";

export interface Measurement {
  readonly id: string;
  value: number;
  unit: Unit;
}

export function average(records: readonly Measurement[]): number {
  if (records.length === 0) {
    return 0;
  }
  return records.reduce((t, r) => t + r.value, 0) / records.length;
}

export const defaultUnit = "C";
```

Running `tsc` in the project directory produces two files. `dist/measurement.js`:

```javascript
export function average(records) {
    if (records.length === 0) {
        return 0;
    }
    return records.reduce((t, r) => t + r.value, 0) / records.length;
}
export const defaultUnit = "C";
```

And `dist/measurement.d.ts`:

```typescript
export type Unit = "C" | "Pa" | "%";
export interface Measurement {
    readonly id: string;
    value: number;
    unit: Unit;
}
export declare function average(records: readonly Measurement[]): number;
export declare const defaultUnit = "C";
```

The two complete each other. The JavaScript file has no types, it has the
implementation; the declaration file has no implementation, it has the types. The word
`declare` says exactly this: "this name exists, this is its type, its definition is
not here."

This split is the package-level counterpart of the type erasure principle from the
first lesson. The type layer is separated from the published code; to a consumer not
reading it, the package is an ordinary JavaScript package.

The `rootDir` setting in this lesson has to be written explicitly together with
`outDir` in this compiler version; when it is not, a `TS5011` diagnostic appears. The
setting's role is determining the output directory layout: `src/measurement.ts`
becomes `dist/measurement.js`, not `dist/src/measurement.js`.

## Ambient Module Declaration

When a library carrying no type information is used, its types can be written in the
project. This is called an **ambient declaration** — declaring the type of something
whose implementation is elsewhere.

Let a `types` directory be added to the configuration:

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true
  },
  "include": ["src", "types"]
}
```

The `types/measurement-driver.d.ts` file declares the names the library offers:

```typescript
declare module "measurement-driver" {
  export interface DriverSettings {
    address: string;
    timeout?: number;
  }

  export function connect(settings: DriverSettings): Promise<void>;
  export function read(channel: string): Promise<number>;
}
```

After this declaration, the module can be used in a typed way inside
`src/app.ts`:

```typescript
import { connect, read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  await connect({ address: "10.0.0.7:9000" });
  return read("temperature.C");
}

export async function invalidCall(): Promise<number> {
  await connect({ address: "10.0.0.7:9000", retryCoun: 3 });
  return read(42);
}
```

```text
src/app.ts(9,45): error TS2353: Object literal may only specify known properties, and 'retryCoun' does not exist in type 'DriverSettings'.
src/app.ts(10,15): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
```

A hand-written declaration is a real contract for the compiler, and all checks apply.

A warning is needed: this declaration's correctness is not verified. If the library
actually behaves differently, the compiler cannot know. An ambient declaration is a
**debt** like the `as` notation and a type predicate; it has to be updated together
when the library's version changes. This is why a hand-written declaration is not
preferred in cases where the library publishes its own declarations.

## Global Scope Declaration

Some names do not come from a module; they are placed into global scope by the
runtime. A `declare global` block declares these:

```typescript
declare global {
  interface MeasurementEnvironment {
    readonly nodeName: string;
    readonly samplingRate: number;
  }

  const measurementEnvironment: MeasurementEnvironment;
}

export {};
```

The trailing `export {};` line is required: it makes the file count as a module, and a
`declare global` block can only be written in module files.

After the declaration, the name can be used without an `import`:

```typescript
export function environmentSummary(): string {
  return `${measurementEnvironment.nodeName} @ ${measurementEnvironment.samplingRate}Hz`;
}

export function invalidAccess(): string {
  return measurementEnvironment.unknownField;
}
```

```text
src/environment.ts(6,33): error TS2339: Property 'unknownField' does not exist on type 'MeasurementEnvironment'.
```

A global scope declaration is a powerful but carefully used tool: the declared name
becomes visible in every file of the project, and where it comes from cannot be
understood just by reading the code. If a name does not genuinely need to be global,
exporting it from a module is preferred.

## Extending Outside Types

Declaration merging, introduced in the Interfaces lesson, finds its real use here. A
library's type can be extended without touching the library. The
`types/extension.d.ts` file opens a second block with the same module name:

```typescript
declare module "measurement-driver" {
  interface DriverSettings {
    retryCount?: number;
  }
}
```

Ambient declarations carrying the same module name merge; since `DriverSettings` is
also an interface, it is open to merging too. As a result, the field becomes usable
without the original declaration changing at all:

```typescript
import { connect, read } from "measurement-driver";

export async function firstReading(): Promise<number> {
  await connect({ address: "10.0.0.7:9000", retryCount: 3 });
  return read("temperature.C");
}

export function environmentSummary(): string {
  return `${measurementEnvironment.nodeName} @ ${measurementEnvironment.samplingRate}Hz`;
}
```

This file checks without error alongside the previous three declaration files.

That merging is really being applied is seen when the field name is misspelled. When
`retryCoun` is written instead of `retryCount` in the same file:

```text
src/app.ts(4,45): error TS2561: Object literal may only specify known properties, but 'retryCoun' does not exist in type 'DriverSettings'. Did you mean to write 'retryCount'?
```

When the extension file is removed from the project, the diagnostic for the same line
changes:

```text
src/app.ts(4,45): error TS2353: Object literal may only specify known properties, and 'retryCoun' does not exist in type 'DriverSettings'.
```

The difference between the two diagnostics — one has a suggestion, the other does not
— directly shows whether merging was applied.

The limit of extension is that it only works on **interfaces**. If a library defines
its type with `type`, it cannot be extended; the `TS2300` diagnostic from the Type
Aliases lesson appears. The practical consequence for library authors: if the shapes
of exposed objects are declared with an interface, consumers can extend them.

## Summary

- A declaration file carries only types; the word `declare` says a name exists but its
  definition is elsewhere.
- The `declaration` option generates declaration files from source; in this compiler
  version, `rootDir` has to be written together with `outDir`.
- An ambient module declaration types a library with no type information; its
  correctness is not verified and it is a debt that has to be updated with the
  library.
- A `declare global` block declares names in global scope; the file needs an
  `export {};` line to count as a module.
- A `declare module` block carrying an `import` line extends an existing module's
  types; extension only works on interfaces.

## Next Step

An ambient declaration was the way to type an untyped library from outside. What
about the project's **own** JavaScript files? An existing codebase cannot be
converted to TypeScript overnight; this migration has to be done gradually. The next
lesson covers checking JavaScript files, typing them with comments, and the migration
strategy.
