Topics
v5.0.0
On this page · sections

Module & Declaration System

TypeScript’s module system bridges JavaScript modules (ESM/CJS) with a declaration layer (.d.ts, declare, augmentation) that describes types for code TypeScript never compiles — including third-party packages and global browser APIs.

ArtifactPurposeCompiled to JS?
.ts / .tsxImplementation + typesYes
.d.tsTypes onlyNo (erased)
declareAmbient type assertionNo
export / importES module boundariesYes (as ESM/CJS)
namespaceLegacy scoped groupingRarely in modern code

ES modules in TypeScript

Definition

// user.ts export type User = { id: string; name: string }; export function formatUser(user: User): string { return `${user.name} (${user.id})`; } // app.ts import { formatUser, type User } from "./user";

import type and export type are type-only imports — erased entirely at compile time (important for bundlers and verbatimModuleSyntax).

module vs moduleResolution

{ "compilerOptions": { "module": "ESNext", "moduleResolution": "bundler" } }
  • module — what JS format TypeScript emits.
  • moduleResolution — how TypeScript resolves import paths (node16, bundler, nodenext).

Interview: Modern Vite/Next apps use "moduleResolution": "bundler". Node libraries publishing dual ESM/CJS use "nodenext".

Declaration files (.d.ts)

Definition

A .d.ts file describes the shape of JavaScript without implementing it:

// lodash.debounce.d.ts declare function debounce<T extends (...args: never[]) => unknown>( fn: T, wait: number, ): T & { cancel(): void }; export default debounce;

Consumers get types; the actual implementation lives in .js.

When TypeScript generates .d.ts

{ "compilerOptions": { "declaration": true, "declarationMap": true, "outDir": "dist" } }

Publishing a library:

dist/ index.js ← runtime index.d.ts ← types for consumers index.d.ts.map ← jump-to-source in IDEs

declarationMap lets users Cmd+click into your original .ts source from node_modules.

The declare keyword

Definition

declare tells TypeScript “this thing exists at runtime — trust me, don’t emit code for it.”

Ambient variables

// env.d.ts declare const __APP_VERSION__: string; declare const __DEV__: boolean;

Vite/Webpack inject these at build time; declare prevents “cannot find name” errors.

Ambient modules (shimming untyped packages)

// types/legacy-widget.d.ts declare module "legacy-widget" { export function init(container: HTMLElement): void; export type WidgetConfig = { theme: "light" | "dark" }; }

Use when a package has no types and you can’t migrate yet. Prefer @types/* or contributing types upstream.

Global augmentation

// global.d.ts export {}; declare global { interface Window { analytics?: { track: (event: string, props?: Record<string, unknown>) => void; }; } } window.analytics?.track("page_view");

The export {} makes the file a module (required for declare global).

Declaration merging

Definition

TypeScript merges multiple declarations with the same name:

// models/user.ts interface User { id: string; } // models/user-profile.ts interface User { name: string; } // Merged: { id: string; name: string } const u: User = { id: "1", name: "Ada" };

Interfaces merge; type aliases do not. Namespace and enum merging exist but are rare in modern ESM codebases — prefer ES modules.

Module augmentation

Definition

Extend types exported by an existing module without modifying its source:

// types/express.d.ts import "express"; declare module "express-serve-static-core" { interface Request { userId?: string; } }

Now every Request in your app includes userId.

Augmenting your own modules

Rare in app code — more commonly used to extend third-party libraries without forking them.

Real-world example — typed i18n

// i18n.d.ts import "react-i18next"; declare module "react-i18next" { interface CustomTypeOptions { defaultNS: "common"; resources: { common: typeof import("./locales/en/common.json"); }; } }

Typed translation keys flow from JSON resources into t("...") calls.

Interview: Module augmentation is how you add user to Express Request or typed i18n keys — without forking the library.

isolatedModules and type-only exports

{ "compilerOptions": { "isolatedModules": true, "verbatimModuleSyntax": true } }

Each file must be transpilable independently (required by SWC/esbuild). Use import type / export type explicitly:

import type { User } from "./models";

Prefer types in tsconfig.json over legacy /// <reference types="..." /> directives.

Senior Interview Summary

What to say

.d.ts files describe JavaScript without compiling it. declare asserts ambient existence. Declaration merging combines interface/namespace declarations. Module augmentation extends third-party types safely. Modern apps use ESM + import type, with global .d.ts only for true globals.

Layers

.ts / .tsx → implementation + types (compiled) .d.ts → types only (consumed or emitted) declare → ambient assertion (no emit) declare module → shim or augment external package declare global → extend Window, etc.

Anti-patterns

Editing node_modules types → use augmentation instead any in .d.ts shims → defeats the purpose Global .d.ts for app types → colocate with modules Mixing namespace + ESM → pick ESM in greenfield code

Practical rule

Library publish → declaration: true + declarationMap Untyped dependency → declare module shim or @types Extend library API → module augmentation in types/ App globals only → declare global in one global.d.ts

Last updated on