Back to Blog
TypeScriptModulesES ModulesCommonJS

TypeScript Module Systems Explained

Understand TypeScript module systems. From CommonJS to ES Modules to configuration and interoperability.

B
Bootspring Team
Engineering
January 3, 2022
5 min read

Understanding module systems is essential for TypeScript projects. Here's how they work and how to configure them.

Module Formats#

1// ES Modules (ESM) - Modern standard 2export const value = 42; 3export function greet(name: string) { 4 return `Hello, ${name}`; 5} 6export default class User {} 7 8import { value, greet } from './module'; 9import User from './module'; 10import * as utils from './module'; 11 12// CommonJS (CJS) - Node.js traditional 13module.exports = { value: 42 }; 14module.exports.greet = function(name) { return `Hello, ${name}`; }; 15 16const { value, greet } = require('./module'); 17 18// TypeScript emits based on tsconfig "module" setting

tsconfig Module Options#

1// tsconfig.json 2{ 3 "compilerOptions": { 4 // Module system for output 5 "module": "esnext", // ES modules 6 // "module": "commonjs", // CommonJS 7 // "module": "node16", // Node.js ESM support 8 9 // Module resolution strategy 10 "moduleResolution": "bundler", // Modern bundlers 11 // "moduleResolution": "node16", // Node.js 12 // "moduleResolution": "node", // Legacy Node.js 13 14 // Allow default imports from CJS 15 "esModuleInterop": true, 16 17 // Allow importing JSON 18 "resolveJsonModule": true, 19 20 // Type-only imports 21 "verbatimModuleSyntax": true, 22 23 // Path aliases 24 "baseUrl": ".", 25 "paths": { 26 "@/*": ["src/*"], 27 "@components/*": ["src/components/*"] 28 } 29 } 30}

ES Modules in Node.js#

1// package.json 2{ 3 "name": "my-package", 4 "type": "module", // Enable ESM 5 "exports": { 6 ".": { 7 "import": "./dist/index.js", 8 "require": "./dist/index.cjs", 9 "types": "./dist/index.d.ts" 10 }, 11 "./utils": { 12 "import": "./dist/utils.js", 13 "types": "./dist/utils.d.ts" 14 } 15 }, 16 "main": "./dist/index.cjs", 17 "module": "./dist/index.js", 18 "types": "./dist/index.d.ts" 19}
1// tsconfig.json for Node.js ESM 2{ 3 "compilerOptions": { 4 "target": "ES2022", 5 "module": "node16", 6 "moduleResolution": "node16", 7 "outDir": "dist", 8 "declaration": true, 9 "esModuleInterop": true, 10 "strict": true 11 } 12} 13 14// Note: Must use .js extensions in imports 15import { helper } from './utils.js'; // Even for .ts files

Import/Export Patterns#

1// Named exports 2export const API_URL = 'https://api.example.com'; 3export type User = { id: string; name: string }; 4export interface Config { debug: boolean } 5export function fetchData() {} 6export class ApiClient {} 7 8// Default export 9export default function main() {} 10 11// Re-exports 12export { helper } from './helper'; 13export * from './utils'; 14export * as utils from './utils'; 15export { default as Button } from './Button'; 16 17// Type-only imports (TypeScript 3.8+) 18import type { User } from './types'; 19import { type Config, fetchData } from './api'; 20 21// Import assertions (for JSON, etc.) 22import data from './data.json' assert { type: 'json' }; 23 24// Dynamic imports 25const module = await import('./heavy-module'); 26const { feature } = await import(`./features/${name}`);

CommonJS Interoperability#

1// tsconfig.json 2{ 3 "compilerOptions": { 4 "esModuleInterop": true, // Enable interop 5 "allowSyntheticDefaultImports": true 6 } 7} 8 9// Without esModuleInterop 10import * as express from 'express'; 11const app = express(); 12 13// With esModuleInterop 14import express from 'express'; 15const app = express(); 16 17// Import CommonJS module 18import cjsModule from 'cjs-package'; 19 20// Import CJS with named exports 21import { namedExport } from 'cjs-package'; 22 23// When authoring dual packages 24// index.ts 25export function helper() {} 26export default helper; 27 28// Compiles to CJS that works both ways: 29// const { helper } = require('package'); 30// import { helper } from 'package'; 31// import helper from 'package';

Path Aliases#

1// tsconfig.json 2{ 3 "compilerOptions": { 4 "baseUrl": ".", 5 "paths": { 6 "@/*": ["src/*"], 7 "@components/*": ["src/components/*"], 8 "@utils/*": ["src/utils/*"], 9 "@types": ["src/types/index"] 10 } 11 } 12} 13 14// Usage 15import { Button } from '@components/Button'; 16import { formatDate } from '@utils/date'; 17import type { User } from '@types'; 18 19// For bundlers, configure alias resolution: 20 21// vite.config.ts 22import { defineConfig } from 'vite'; 23import path from 'path'; 24 25export default defineConfig({ 26 resolve: { 27 alias: { 28 '@': path.resolve(__dirname, 'src'), 29 '@components': path.resolve(__dirname, 'src/components'), 30 }, 31 }, 32}); 33 34// webpack.config.js 35module.exports = { 36 resolve: { 37 alias: { 38 '@': path.resolve(__dirname, 'src'), 39 }, 40 }, 41}; 42 43// jest.config.js 44module.exports = { 45 moduleNameMapper: { 46 '^@/(.*)$': '<rootDir>/src/$1', 47 }, 48};

Barrel Files#

1// components/index.ts (barrel file) 2export { Button } from './Button'; 3export { Card } from './Card'; 4export { Modal } from './Modal'; 5export type { ButtonProps } from './Button'; 6 7// Usage 8import { Button, Card, Modal } from '@/components'; 9 10// Caution: Can hurt tree-shaking and build performance 11 12// Better for large projects: direct imports 13import { Button } from '@/components/Button'; 14 15// Or selective barrel exports 16// components/forms/index.ts 17export { Input } from './Input'; 18export { Select } from './Select'; 19 20// Import specific barrel 21import { Input, Select } from '@/components/forms';

Module Augmentation#

1// Extend existing module 2declare module 'express' { 3 interface Request { 4 user?: User; 5 session?: Session; 6 } 7} 8 9// Extend global 10declare global { 11 interface Window { 12 analytics: Analytics; 13 } 14 15 namespace NodeJS { 16 interface ProcessEnv { 17 NODE_ENV: 'development' | 'production'; 18 API_URL: string; 19 } 20 } 21} 22 23// Must export to make it a module 24export {}; 25 26// Extend third-party types 27import 'styled-components'; 28 29declare module 'styled-components' { 30 export interface DefaultTheme { 31 colors: { 32 primary: string; 33 secondary: string; 34 }; 35 } 36}

Dual Package Publishing#

1// Build both ESM and CJS 2// tsup.config.ts 3import { defineConfig } from 'tsup'; 4 5export default defineConfig({ 6 entry: ['src/index.ts'], 7 format: ['cjs', 'esm'], 8 dts: true, 9 splitting: false, 10 sourcemap: true, 11 clean: true, 12}); 13 14// package.json 15{ 16 "name": "my-library", 17 "version": "1.0.0", 18 "type": "module", 19 "main": "./dist/index.cjs", 20 "module": "./dist/index.js", 21 "types": "./dist/index.d.ts", 22 "exports": { 23 ".": { 24 "import": { 25 "types": "./dist/index.d.ts", 26 "default": "./dist/index.js" 27 }, 28 "require": { 29 "types": "./dist/index.d.cts", 30 "default": "./dist/index.cjs" 31 } 32 } 33 }, 34 "files": ["dist"] 35}

Best Practices#

Configuration: ✓ Use "moduleResolution": "bundler" for apps ✓ Use "moduleResolution": "node16" for Node.js ✓ Enable esModuleInterop ✓ Use verbatimModuleSyntax Imports: ✓ Use type-only imports when possible ✓ Prefer named exports over default ✓ Use path aliases consistently ✓ Avoid deep barrel re-exports Publishing: ✓ Ship both ESM and CJS ✓ Include type definitions ✓ Use exports field properly ✓ Test in both module systems

Conclusion#

Understanding TypeScript's module systems helps you configure projects correctly and write interoperable code. Use ES Modules for modern projects, configure esModuleInterop for CommonJS compatibility, and set up proper dual-package exports when publishing libraries.

Share this article

Help spread the word about Bootspring