Setup Summary

TypeScript modules, resolution and bundlers

Two settings in tsconfig.json

SettingQuestion it answers
moduleWhat import/export style does the output JS use?
moduleResolutionHow does TypeScript find the file for import x from "./db"?

module values

  • commonjs: output uses require() (old Node style)
  • esnext / es2022: output uses import/export, but assumes a bundler or browser runs it
  • nodenext: follows Node. "type": "module" in package.json gives ESM, otherwise CommonJS
  • Note: this is what typescript compile to

moduleResolution values

  • node (also called node10): old CommonJS rules, no file extension needed. Being deprecated in newer TypeScript.
  • nodenext: modern Node rules. Relative imports in ESM need the extension: ./db.js
    • so we need to do import something from "file.js"
  • bundler: no extension needed, trusts a bundler to find the files

What was there before

  • Old default: module: commonjs + moduleResolution: node
  • nodenext was added in TypeScript 4.7 (2022) because Node got real ESM support with stricter rules

Convention now

SituationmodulemoduleResolution
Node backend, run with plain nodeNodeNextNodeNext
Frontend with a bundler (Vite, SvelteKit)ESNextBundler
Old Node project (CommonJS)CommonJSNode
  • The two settings go as a pair. Do not mix them.
  • New project: use ESM ("type": "module").
  • Existing project: keep what it already uses. Check package.json.
  • .mjs = always ESM, .cjs = always CommonJS

ESM things to remember

  • Import with .js even though the source is .ts: import { x } from "./db.js"
  • Top-level await works
  • No __dirname. Use import.meta.dirname

Why Node does not need a bundler

  • tsc does NOT bundle. It translates each .ts to one .js, file by file.
  • Node reads files from the local disk at startup (fast), including node_modules.
  • So node dist/server.js just works.

Why a browser needs a bundler

  • Every file is a network request, so hundreds of files is slow.
  • A browser cannot read node_modules, and does not understand TypeScript or .svelte.

What a bundler does

  1. Starts at the entry file and follows every import
  2. Joins the code into a few files
  3. Transforms TypeScript, Svelte, SCSS into plain JS/CSS
  4. Shrinks the code (minify) and removes unused code (tree shaking)
  5. Handles images and CSS, with fingerprinted names for caching

tsc = translator and type checker. Bundler = translator + packer.

Bundler examples

  • Vite: most popular for frontends. SvelteKit uses it.
  • esbuild: very fast, written in Go. Used inside tsx and vitest.
  • webpack: older, common in existing projects, slower
  • Rollup, Parcel, Turbopack, tsup: other options (tsup is popular for Node libraries)

When backends bundle anyway

  • One small file for serverless (AWS Lambda), faster start
  • Faster builds in big repos
  • Smaller Docker images

Note: most bundlers do NOT check types. Teams often run tsc --noEmit for type checks and a bundler to build.