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" settingtsconfig 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 filesImport/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.