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:
.jsextension in imports (import "./foo.js"even in TS source)- top-level await OK
- no
__dirname/__filename(useimport.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"
}
}
tsup (recommended)
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
.jsextension in ESM imports. "type": "module"but importing CJS-only packages without interop.- Missing
exportsfield — modern resolvers can’t find your code. - Mixing default and named imports for CJS interop.
import.meta.urlin 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 .