Modern ESM import/export patterns. Use when writing or reviewing module structure.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers modern ECMAScript Module (ESM) patterns for TypeScript projects.
Use this skill when:
ESM ONLY - No CommonJS (require/module.exports). All projects use native ES Modules.
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"]
}
// ✅ Named imports - clear and tree-shakeable
import { formatDate, parseDate } from './utils/date.js';
import { UserService, type User } from './services/user.js';
// ✅ Type-only imports
import type { Config } from './config.js';
// ✅ Default import for single main export
import express from 'express';
import React from 'react';
// ✅ Default with named
import fs, { promises as fsp } from 'node:fs';
// ✅ Namespace import when many exports needed
import * as utils from './utils/index.js';
utils.formatDate(date);
utils.parseDate(str);
// ✅ Dynamic import for code splitting
const module = await import('./heavy-module.js');
// ✅ Conditional loading
if (process.env.NODE_ENV === 'development') {
const devTools = await import('./dev-tools.js');
devTools.enable();
}
// ✅ Named exports - explicit and tree-shakeable
export function formatDate(date: Date): string {
return date.toISOString();
}
export interface User {
id: string;
name: string;
}
export const DEFAULT_TIMEOUT = 5000;
// ✅ Default export for main module entry
export default class ApiClient {
// ...
}
// ✅ Default export for React components
export default function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
// ✅ Re-export from barrel file (index.ts)
export { formatDate, parseDate } from './date.js';
export { User, UserService } from './user.js';
export type { Config } from './config.js';
// ✅ Re-export with rename
export { internalFunction as publicFunction } from './internal.js';
// ✅ Re-export all
export * from './utils.js';
export * as helpers from './helpers.js';
Always use .js extension in imports, even for TypeScript files:
// ✅ Correct - .js extension
import { helper } from './utils/helper.js';
import { User } from '../models/user.js';
// ❌ Wrong - no extension
import { helper } from './utils/helper';
// ❌ Wrong - .ts extension
import { helper } from './utils/helper.ts';
Why .js? TypeScript compiles .ts to .js, and Node.js ESM requires extensions.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true
}
}
src/
├── index.ts # Main entry point
├── types/
│ └── index.ts # Type exports
├── utils/
│ ├── index.ts # Barrel file
│ ├── date.ts
│ └── string.ts
├── services/
│ ├── index.ts # Barrel file
│ ├── api.ts
│ └── auth.ts
└── models/
├── index.ts # Barrel file
└── user.ts
Use barrel files to simplify imports:
// src/utils/index.ts
export { formatDate, parseDate } from './date.js';
export { capitalize, truncate } from './string.js';
export { debounce, throttle } from './async.js';
// Consumer code
import { formatDate, capitalize, debounce } from './utils/index.js';
// or
import { formatDate, capitalize, debounce } from './utils/index.js';
Use node: prefix for Node.js built-in modules:
// ✅ With node: prefix
import fs from 'node:fs';
import path from 'node:path';
import { createServer } from 'node:http';
// ❌ Without prefix (works but not recommended)
import fs from 'fs';
When importing CommonJS modules:
// ✅ Default import for CommonJS modules
import lodash from 'lodash';
// ✅ Named imports if supported
import { debounce } from 'lodash';
// ⚠️ May need default for some CJS modules
import pkg from 'some-cjs-package';
const { namedExport } = pkg;
// ❌ Never use require() in ESM
const fs = require('fs');
// ❌ Never use module.exports
module.exports = { foo };
// ❌ Never use __dirname/__filename directly
console.log(__dirname);
// ✅ ESM way to get __dirname and __filename
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// ❌ Avoid circular imports
// a.ts
import { b } from './b.js';
export const a = b + 1;
// b.ts
import { a } from './a.js'; // Circular!
export const b = a + 1;
// ✅ Break cycle with third module or restructure
// shared.ts
export const base = 1;
// a.ts
import { base } from './shared.js';
export const a = base + 1;
// b.ts
import { base } from './shared.js';
export const b = base + 2;
Use type keyword for imports used only in type positions:
// ✅ Type-only import - removed at compile time
import type { User, Config } from './types.js';
// ✅ Inline type import
import { createUser, type User } from './user.js';
// ✅ Type-only re-export
export type { User, Config } from './types.js';
"type": "module" in package.json.js extension in all importsnode: prefix for Node.js built-ins"type": "module".js extensionrequire() or module.exportsnode: prefixtype keyword