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.
| Artifact | Purpose | Compiled to JS? |
|---|---|---|
.ts / .tsx | Implementation + types | Yes |
.d.ts | Types only | No (erased) |
declare | Ambient type assertion | No |
export / import | ES module boundaries | Yes (as ESM/CJS) |
namespace | Legacy scoped grouping | Rarely 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 resolvesimportpaths (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 IDEsdeclarationMap 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
userto ExpressRequestor 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.tsfiles describe JavaScript without compiling it.declareasserts ambient existence. Declaration merging combines interface/namespace declarations. Module augmentation extends third-party types safely. Modern apps use ESM +import type, with global.d.tsonly 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 codePractical 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