Modules
|
This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
TypeScript builds on the ES module system: any file with a top-level import or export is a module, and everything else is a script. This page covers module syntax, the type-only forms, the module / moduleResolution compiler options, path mapping, CommonJS interop, ambient module declarations, and the legacy namespace construct. Read the handbook’s Modules chapter alongside the deeper Modules — Theory and Modules — Reference guides; the older model is described in Namespaces and Modules.
For the runtime semantics of ES modules themselves, see Modules.
ES module syntax
// math.ts -- named exports
export const PI = 3.14159;
export function area(r: number): number {
return PI * r * r;
}
// logger.ts -- a default export plus a named one
export default class Logger {
log(msg: string): void { console.log(msg); }
}
export const LEVELS = ["debug", "info", "warn"] as const;
// app.ts -- consuming both styles
import Logger, { LEVELS } from "./logger.js";
import { PI, area } from "./math.js";
import * as math from "./math.js"; // namespace import: math.area(...)
new Logger().log(`PI=${PI}, area=${area(2)}, levels=${LEVELS.length}, ${math.PI}`);
Re-exports
// index.ts -- a barrel that forwards other modules' exports
export { area, PI } from "./math.js";
export { default as Logger } from "./logger.js";
export * from "./shapes.js"; // re-export all named exports
export * as shapes from "./shapes.js"; // re-export as a single namespace object
Dynamic import
import() is a function-like expression that returns a Promise of the module namespace. Use it for code-splitting or conditional loading; the specifier can be computed at runtime.
async function loadLocale(name: string): Promise<Record<string, string>> {
const mod = await import(`./locales/${name}.js`);
return mod.default;
}
// Types flow through: `mod` is typed as the target module's shape
const { area } = await import("./math.js");
Module vs. script mode
A file with no top-level import or export is a script: its declarations go into the global scope and can collide with other scripts. Add an empty export \{} to force module mode.
// util.ts -- without the next line, `helper` would be a global
export {};
function helper() { /* ... */ }
Type-only imports and exports
import type / export type mark a binding as existing only in the type system; it is erased from the JavaScript output. This prevents accidental runtime dependencies and side-effect imports, and is required for correct output under single-file transpilers.
import type { User } from "./models.js"; // whole import is type-only
import { createUser, type UserId } from "./models.js"; // inline: value + type in one statement
export type { User };
export { makeId, type UserId as Id };
// `import type` bindings cannot be used as values -- this would be an error:
// const u = new User();
verbatimModuleSyntax (the modern setting, replacing importsNotUsedAsValues and preserveValueImports) makes emit purely mechanical: any import / export without the type keyword is kept verbatim, any with it is dropped. You then must write import type for anything used only as a type.
// With verbatimModuleSyntax: true
import { type Shape, draw } from "./shapes.js"; // `Shape` erased, `draw` kept
import type { Config } from "./config.js"; // entire statement erased
isolatedModules tells TypeScript to check that each file can be transpiled on its own (by Babel, esbuild, swc). It bans const enum used across files, plain re-exports of types without the type keyword, and other whole-program tricks. Turn it on whenever a bundler — not tsc — produces your JavaScript. See tsconfig.json and Compiler Options.
module and moduleResolution
The module option controls the emitted module format; moduleResolution controls how import specifiers are found. Modern configs pick a matched pair.
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"resolveJsonModule": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {
"@app/*": ["src/*"],
"@config": ["src/config/index.ts"]
}
}
}
-
node16/nodenext— Node’s dual ESM/CJS algorithm. The file’s format is decided by the nearestpackage.json"type"and the file extension; relative imports must carry an explicit extension (./x.js), andpackage.json"exports"/"imports"are honoured.nodenexttracks the latest Node behaviour,node16pins it. -
bundler— models what esbuild / Vite / webpack do:"exports"is respected but extensionless relative imports are allowed. Pair with"module": "esnext"or"preserve"and let the bundler emit. -
classic— the pre-Node 2013-era walk-up algorithm with nonode_modulesand no"exports"support. Deprecated; never choose it for new code.
node10 (formerly node) is the legacy node_modules resolution without "exports" support — only for old CommonJS projects.
paths and baseUrl
paths maps specifier patterns to on-disk locations for the type checker only, resolved relative to baseUrl (or to the tsconfig under moduleResolution: bundler / node16 / nodenext, where baseUrl is no longer required).
|
|
JSON imports
With resolveJsonModule, a .json file can be imported and is typed from its contents.
import pkg from "./package.json"; // pkg.version: string, statically typed
import { version } from "./package.json" with { type: "json" }; // import attributes, nodenext
Package exports / imports and .js specifiers
Under node16 / nodenext / bundler, TypeScript reads the consumed package’s "exports" map and its "types" condition to locate declarations, and its "imports" map for the package’s own #-prefixed internal specifiers.
{
"name": "mylib",
"type": "module",
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
"./util": { "types": "./dist/util.d.ts", "import": "./dist/util.js" }
},
"imports": {
"#internal/*": "./dist/internal/*.js"
}
}
In .ts source you write the specifier with the .js extension that will exist at runtime — TypeScript resolves it back to the sibling .ts / .d.ts for checking:
import { area } from "./math.js"; // file on disk is math.ts
import { helper } from "#internal/helper.js"; // via the package's own "imports" map
CommonJS <-> ESM interop
A CommonJS module sets module.exports = …; an ES import expects a namespace object with named bindings and a default. Two flags bridge the gap.
-
esModuleInterop— synthesises adefaultfor CJS modules soimport express from "express"works, and makesimport * as nsa true namespace object rather than the baremodule.exports. ImpliesallowSyntheticDefaultImports. -
allowSyntheticDefaultImports— type-checking only: lets you write a default import even when no synthetic default is emitted (for example when a bundler handles the interop at build time).
// With esModuleInterop: true
import express from "express"; // CJS `module.exports = express`, seen as the default
import * as fs from "node:fs"; // namespace of named exports
// Without esModuleInterop you were forced to write the TS-specific CJS import:
import express = require("express"); // still valid, and the correct form under node16 for CJS
Pitfall: if a package’s types declare export default but its runtime is CJS with module.exports = fn, a default import works only with esModuleInterop (or the import x = require(…) form). Mismatched interop settings between a library and its consumer are a common cause of "X is not a function" at runtime.
Ambient module declarations
declare module "name" describes a module that has no .d.ts of its own, or augments an existing one. A wildcard specifier types whole classes of non-code imports handled by a bundler loader.
// globals.d.ts
declare module "untyped-legacy-lib" {
export function connect(url: string): void;
const version: string;
export default version;
}
// Wildcard: every `*.css` import is a record of class-name strings
declare module "*.css" {
const classes: { readonly [name: string]: string };
export default classes;
}
declare module "*.svg" {
const url: string;
export default url;
}
import styles from "./Button.css"; // styles: { readonly [name: string]: string }
import logo from "./logo.svg"; // logo: string
Module augmentation reopens a real module to add members — see Declaration Files.
Namespaces and triple-slash directives
namespace (originally "internal modules") predates ES modules. It groups declarations under one global object, optionally split across files with a path reference directive.
// legacy: shapes.ts
namespace Shapes {
export interface Circle { r: number; }
export const area = (c: Circle) => Math.PI * c.r ** 2;
}
// consumer -- no import, `Shapes` is global
const a = Shapes.area({ r: 2 });
|
Prefer ES modules over namespaces. For application and library code, use |
declare namespace is still idiomatic in hand-written .d.ts to model the shape of a global UMD library, paired with export as namespace.
// jquery-lib.d.ts (sketch)
declare namespace JQueryLib {
interface Options { debug?: boolean; }
function init(opts?: Options): void;
}
export = JQueryLib;
export as namespace JQueryLib;
Triple-slash directives are single-line comments containing an XML tag, valid only at the very top of a file. The ones still in use:
-
/// <reference types="node" />— pull in an@typespackage’s global declarations from inside another.d.tswhere you cannot use thetsconfigtypesarray. -
/// <reference lib="es2023.array" />— depend on a specific slice of the built-in lib. -
/// <reference path="./other.d.ts" />— legacy file ordering for non-module.d.ts; avoid in module code. -
/// <reference no-default-lib="true"/>— only when authoring a replacementlib.d.ts.
In ordinary import-using source you rarely need any of these; the tsconfig types / lib arrays and normal imports cover it.
Module resolution at a glance
no node_modules, no exports"] B -->|"node10 / node (legacy)"| D["node_modules walk,
ignores package exports"] B -->|"node16 / nodenext"| E{"Relative specifier?"} B -->|"bundler"| F["node_modules plus package exports,
extensionless imports allowed"] E -->|"yes"| G["Require explicit extension,
write .js for a .ts file"] E -->|"no"| H["Resolve via package.json
exports / imports plus types condition"] G --> I["Check against sibling .ts / .d.ts"] H --> I F --> I D --> I C --> I
Project References and Build covers how resolution interacts with composite builds and tsc --build.