Modules cheatsheet.

Import / export

// named
export function foo() {}
export const bar = 42;

// default
export default class Widget {}

// re-export
export { foo } from "./mod";
export * from "./mod";
export type { User } from "./types";

// import
import { foo, bar } from "./mod";
import Widget from "./widget";
import * as mod from "./mod";
import type { User } from "./types";

Type-only import / export

import type { User } from "./types";
export type { Foo };

// Inline
import { type User, fn } from "./mod";

With verbatimModuleSyntax: true, must mark type-only — otherwise emits a runtime import.

ESM in Node

package.json:

{
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

ESM in Node requires:

  • .js extension in imports (import "./foo.js" even in TS source)
  • top-level await OK
  • no __dirname / __filename (use import.meta)
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

CJS

{ "type": "commonjs" }
// import works with esModuleInterop
import fs from "fs";
const x = require("./y");          // OK in CJS

Dual package (ESM + CJS)

{
  "name": "mylib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    }
  }
}

Build both via tsup or two tsc runs.

exports field

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js",
    "./package.json": "./package.json"
  }
}

Restricts what consumers can import. Anything not in exports becomes inaccessible.

Conditional exports

{
  "exports": {
    ".": {
      "node": "./dist/node.js",
      "browser": "./dist/browser.js",
      "default": "./dist/index.js"
    }
  }
}

Subpath patterns

{
  "exports": {
    "./locales/*.json": "./dist/locales/*.json"
  }
}
npm i -D tsup

tsup.config.ts:

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
  target: "node20",
  splitting: false,
  sourcemap: true,
});
npx tsup

Outputs both ESM and CJS with .d.ts files.

Self-referencing

With exports, your own package can import itself:

import { foo } from "mylib/utils";

Within mylib’s source. Useful for tests.

.js extensions in TS source

ESM in Node requires explicit .js:

import { foo } from "./foo.js";       // not "./foo"

TypeScript follows the runtime; .js resolves to .ts during build.

allowImportingTsExtensions

For when you write .ts explicitly:

{
  "allowImportingTsExtensions": true,
  "noEmit": true                   // required
}

Mostly for bundler workflows.

moduleResolution: bundler

{
  "module": "ESNext",
  "moduleResolution": "bundler",
  "allowImportingTsExtensions": true,
  "noEmit": true
}

Best for projects bundled by Vite/esbuild/Next.

Side-effect imports

import "./polyfill";        // runs the file

Beware: verbatimModuleSyntax keeps these in output. Bundlers may tree-shake unless flagged in package.json sideEffects: false.

Dynamic imports

const mod = await import("./mod");
mod.foo();

Returns a Promise. Useful for code splitting.

Wildcard module declarations

// global.d.ts
declare module "*.svg" {
  const content: string;
  export default content;
}

declare module "*.css" {}

Augmenting a module

import "express";

declare module "express" {
  interface Request {
    user?: { id: number };
  }
}

Common mistakes

  • Forgetting .js extension in ESM imports.
  • "type": "module" but importing CJS-only packages without interop.
  • Missing exports field — modern resolvers can’t find your code.
  • Mixing default and named imports for CJS interop.
  • import.meta.url in CJS — undefined.

Read this next

If you want my dual-build library template, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .