Skip to content
intermediate Phase 13 · TypeScript Build & Tooling

ESM & Module Resolution

Configure ES modules, module resolution, and path aliases.

1h
0 problems
Topic Progress 0%

ESM Semantics in TypeScript

ESM Semantics in TypeScript

ESM is statically analyzed: imports are hoisted, bindings are live, and this at module top level is undefined. TypeScript mirrors that with import/export and erases types at emit — but how it emits depends on module:

{
  "type": "module",
  "compilerOptions": { "module": "nodenext", "moduleResolution": "nodenext" }
}

With nodenext, Node decides the format from type and file extension: .ts beside "type": "module" emits ESM. Relative specifiers must carry .js (import { f } from "./f.js") because Node does not guess extensions — the file on disk is what gets loaded.

Resolution Options and Path Aliases

Resolution Options and Path Aliases

Match resolution to the loader:

{ "moduleResolution": "nodenext" }  // Node ESM/CJS, honors "exports"
{ "moduleResolution": "bundler" }   // Vite/webpack: extensionless, alias-driven
{ "moduleResolution": "node10" }    // legacy node_modules walking only

Bare specifiers resolve through the dependency's exports map under nodenext; a package that only ships main may fail there while working under bundler. Aliases come from paths:

{ "paths": { "@ui/*": ["./src/ui/*"] } }

Vite needs a matching resolve.alias, Jest needs moduleNameMapper, and Node needs an import map or a re-barrel — otherwise types compile while runtime resolution fails.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

Extension-less imports compile under bundler and crash under Node ESM with ERR_MODULE_NOT_FOUND. Keep the strictest resolution the deployment uses.

Dual package hazard. Shipping both ESM and CJS builds can load two module instances — one for import, one for require — breaking instanceof and singletons. Exports maps with import/require conditions plus a shared state module (or ESM-only) avoid it.

CJS globals vanish. __dirname and require do not exist in ESM; use import.meta.url and dynamic import().

Interview: walk a specifier from source through exports conditions to a loaded module, then explain why createRequire is a symptom, not a fix.

Quiz

1. Why does import { f } from "./f" fail in Node ESM?

Question 1 options

2. What do the import and require conditions in a package exports map do?

Question 2 options

3. Where must path aliases be mirrored for the application to actually run?

Question 3 options

Flashcards

Question

moduleResolution: nodenext

Answer

Node rules: explicit .js extensions, honors package exports conditions, format from type field.

Question

Dual package hazard

Answer

Loading a package once as ESM and once as CJS yields two instances and breaks identity checks.

Question

exports map

Answer

Package entry table with import/require/types conditions controlling what each loader receives.

Question

import.meta.url

Answer

ESM replacement for __dirname-style path math; __dirname does not exist in ESM.

Question

Live bindings

Answer

ESM exports reflect the exporter's current value — reassignment upstream is visible to importers.

Revision Notes

Key Takeaways

  • 1. Format comes from package type and module setting; resolution comes from moduleResolution
  • 2. Aliases live in three places: compiler, bundler, test runner
  • 3. Shipping both formats invites duplicate-module bugs

Interview Tips

  • • Trace one import end to end and name which tool resolves each hop
  • • Explain dual-package hazard with a concrete instanceof failure

Cheat Sheet

type:module + module/moduleResolution nodenext; relative imports need .js; paths must be mirrored in bundler/Jest; exports conditions split import vs require builds.

Continue Learning