Capítulo 14 de 36

Chapter 14: Modules

Core Idea

Any TypeScript file with a top-level import/export is a module (own scope, nothing implicitly global); TypeScript's own syntax choices ride on top of three independent JS-runtime concerns — syntax, module resolution, and emitted module output format — which is why the same import statement can behave differently depending on tsconfig.json settings.

Key Concepts

  • Module vs. script: a file with no top-level import/export/top-level await is a script — its declarations live in the shared global scope, not module-isolated. Force a file to be treated as a module (without actually exporting anything) via a bare export {};.
  • ES Module syntax essentials: export default for a file's single main export (imported via import name from "./file.js", any local name); named exports (export const x = ...) imported via import { x } from "./file.js", optionally renamed (import { x as y }), mixed with a default (import Default, { x as y } from "./file"), or collected wholesale (import * as ns from "./file"). A bare import "./file" runs the module purely for side effects, binding nothing locally.
  • Type-only imports: types and values share the same export/import syntax, but TypeScript adds two ways to mark an import as type-only so non-TS transpilers (Babel, swc, esbuild) know it's safe to erase entirely:
    • import type { Cat, Dog } from "./animal.js" — the whole statement is type-only; importing a value through it is an error.
    • Inline per-specifier: import { createCatName, type Cat, type Dog } from "./animal.js" (TS 4.5+) — mixes value and type-only imports in one statement.
  • import x = require("x"): ES-Module-syntax sugar that maps 1:1 onto a CommonJS require in the emitted output — used when you want TypeScript syntax but guaranteed CommonJS-equivalent semantics.
  • CommonJS: the format most npm packages still ship (module.exports = {...}, consumed via const x = require("./file"), optionally destructured). Worth understanding even if you write ES Module syntax exclusively, since many dependencies are still CJS. esModuleInterop smooths over the default-import vs. namespace-import mismatch between the two systems.
  • Module resolution is the process of turning an import/require string into an actual file on disk — governed by moduleResolution (classic, kept only for backwards compatibility, vs. node, which replicates Node.js CJS resolution plus .ts/.d.ts lookups), together with baseUrl, paths, rootDirs.
  • Module output is a separate axis from syntax/resolution: target controls which JS language features get downleveled for older runtimes; module controls what module-loading code gets emitted (es2020 keeps native ESM import/export; commonjs emits require/module.exports; umd wraps for either). The same source import statement can compile to any of these depending on module.
  • TypeScript namespaces: TS's own pre-ES-Modules module format. Still actively used in .d.ts files for complex type definitions (notably DefinitelyTyped), not deprecated, but the handbook explicitly recommends ES Modules for new code since namespaces mostly duplicate what ES Modules now cover natively.

Code Examples

// animal.ts
export type Cat = { breed: string };
export const createCatName = () => "fluffy";

// app.ts
import { createCatName, type Cat } from "./animal.js"; // mixed value + type-only import
  • What it demonstrates: the TS 4.5+ inline type modifier marking Cat as type-only within an otherwise normal import statement, so a value-stripping transpiler knows exactly what to erase.

Reference Tables

Compiler optionControls
moduleResolutionhow an import string maps to a file on disk (classic vs node)
moduleemitted module-loading code (es2020, commonjs, umd, ...)
targetwhich JS language features get downleveled
esModuleInteropsmooths CJS/ESM default-vs-namespace-import mismatches

Key Takeaways

  1. Prefer ES Module syntax (import/export) for new code over TypeScript namespaces — namespaces are legacy, still fine in .d.ts files, not recommended for application code.
  2. Use import type / inline type specifiers when a build pipeline includes a non-type-aware transpiler (Babel/swc/esbuild) that needs to know which imports are safe to strip.
  3. module and target are independent settings — don't conflate "which JS syntax features survive compilation" with "how modules load at runtime."
  4. A file with zero top-level import/export is treated as a global script, not a module — add export {} if that's not the intent.

Connects To

  • Type Declarations: .d.ts ambient module declarations build directly on this chapter's import/export model.
  • Modules Reference (Modules Reference group): the deep-dive on resolution algorithms, theory, and the ESM/CJS interop appendix.
  • Namespaces (Reference): full treatment of the legacy namespace construct mentioned only briefly here.