Publishing Node Modules with TypeScript and ES Modules

A comprehensive guide to creating and publishing production-ready npm packages using modern ES Modules and TypeScript

Introduction: The Modern JavaScript Module Landscape

The JavaScript ecosystem has evolved significantly, and ES Modules (ESM) have emerged as the standard module system for modern web development. Unlike the older CommonJS module format that dominated Node.js for years, ESM provides native syntax for importing and exporting code, enabling better tree shaking, static analysis, and browser compatibility.

Publishing npm packages with TypeScript and ESM requires understanding the interplay between TypeScript's type system, Node.js module resolution, and the npm registry's expectations. The good news is that modern tooling has simplified this process considerably, where developers once needed complex build pipelines with multiple output formats, today's TypeScript and Node.js versions support a streamlined approach that focuses on ESM-first development.

This guide covers everything you need to know to publish production-ready TypeScript npm packages that leverage ES modules, from initial project setup through automated CI/CD deployment. Whether you're building internal tools for your organization or contributing to the open-source community, understanding modern JavaScript development practices will help you create packages that are easy to consume and maintain.

Why ES Modules Matter for npm Packages

ES Modules offer several compelling advantages over CommonJS:

  • Native browser support means your package code can run directly in browsers without transpilation
  • Static analysis capabilities allow bundlers to perform tree shaking, eliminating unused code
  • Consistent syntax across browser and server environments
  • Import conditions in package.json enable fine-grained control over what code loads in different environments

For teams investing in Node.js development services, adopting ESM represents a significant step toward more maintainable and performant codebases.

Benefits of ES Modules for npm Packages

Understanding why ESM is the preferred module system

Native Browser Support

Run package code directly in browsers without transpilation, reducing bundle sizes and improving load times.

Tree Shaking

Bundlers can eliminate unused code from final bundles, resulting in smaller application bundles.

Consistent Syntax

Import and export statements work the same way across Node.js, browsers, and edge environments.

Import Conditions

Fine-grained control over what code loads in different environments through package.json exports.

Project Structure and File Organization

A well-organized project structure is foundational to maintainable npm packages. The recommended layout separates source code, tests, documentation, and build outputs clearly.

Recommended Directory Structure

my-package/
├── README.md # Package documentation
├── LICENSE # License file
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
├── .gitignore # Git ignore rules
├── src/ # TypeScript source files
│ ├── index.ts # Main entry point
│ ├── utils.ts # Utility functions
│ └── types/ # Type definitions
├── test/ # Test files
│ └── index.test.ts
├── dist/ # Compiled JavaScript output
│ ├── index.js
│ ├── index.d.ts
│ └── index.js.map
└── docs/ # Documentation
 └── api/

Source Directory Organization

The src directory should contain TypeScript source files organized logically. Each file should export its public API explicitly:

// src/index.ts - Main public API
export { MyClass, myFunction, MyType } from './my-class';

// src/my-class.ts - Implementation
export class MyClass { /* ... */ }
export function myFunction() { /* ... */ }
export type MyType = { /* ... */ };

Test Organization

Tests can either live alongside source files using naming conventions like *.test.ts or in a separate test directory. The key consideration is ensuring tests are excluded from npm package publication. For comprehensive testing strategies in Node.js projects, explore our Node.js development services that include test-driven development practices and automated testing workflows.

Following established JavaScript project organization patterns helps teams maintain consistency across multiple packages and reduces onboarding time for new contributors.

Recommended Project Structure
my-package/
├── README.md # Package documentation
├── LICENSE # License file
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
├── src/ # TypeScript source files
│ ├── index.ts # Main entry point
│ ├── utils.ts # Utility functions
│ └── types/ # Type definitions
├── test/ # Test files
│ └── index.test.ts
├── dist/ # Compiled JavaScript output
│ ├── index.js
│ ├── index.d.ts
│ └── index.js.map
└── docs/ # Documentation
 └── api/

TypeScript Configuration for ESM

The tsconfig.json file is the heart of TypeScript compilation, and configuring it correctly for ESM packages is crucial.

Essential Compiler Options

{
 "compilerOptions": {
 "target": "ES2022",
 "module": "NodeNext",
 "moduleResolution": "NodeNext",
 "lib": ["ES2022"],
 "declaration": true,
 "declarationMap": true,
 "sourceMap": true,
 "outDir": "./dist",
 "rootDir": "./src",
 "strict": true,
 "esModuleInterop": true,
 "skipLibCheck": true,
 "forceConsistentCasingInFileNames": true,
 "resolveJsonModule": true
 },
 "include": ["src/**/*"],
 "exclude": ["node_modules", "dist", "test"]
}

Key Settings Explained

  • target: Specifies JavaScript version for output code. ES2022 provides modern features while maintaining broad compatibility.
  • module and moduleResolution: Setting both to "NodeNext" tells TypeScript to use Node.js's native ESM module resolution.
  • declaration: Generates .d.ts files alongside compiled JavaScript, providing type information to consumers.
  • declarationMap: Creates source maps for declaration files, enabling "Go to Definition" functionality.
  • strict: Enables all strict type-checking options for better type safety.

Root Directory and Output Directory

The rootDir and outDir settings determine where TypeScript looks for source files and writes compiled output:

  • rootDir: Specifies the base directory for all TypeScript files
  • outDir: Specifies where compiled JavaScript, declarations, and source maps are written

Proper TypeScript configuration is essential for maintainable JavaScript projects and enables features like type-safe npm package development that catches errors at compile time rather than runtime. Teams investing in TypeScript expertise see significant improvements in code quality and developer productivity.

tsconfig.json for ESM Packages
1{2 "compilerOptions": {3 "target": "ES2022",4 "module": "NodeNext",5 "moduleResolution": "NodeNext",6 "lib": ["ES2022"],7 "declaration": true,8 "declarationMap": true,9 "sourceMap": true,10 "outDir": "./dist",11 "rootDir": "./src",12 "strict": true,13 "esModuleInterop": true,14 "skipLibCheck": true,15 "forceConsistentCasingInFileNames": true,16 "resolveJsonModule": true17 },18 "include": ["src/**/*"],19 "exclude": ["node_modules", "dist", "test"]20}

Package.json Configuration

The package.json file controls how npm packages your code and how consumers import it.

Essential package.json Fields

{
 "name": "@your-org/my-package",
 "version": "1.0.0",
 "type": "module",
 "description": "A short description of your package",
 "main": "./dist/index.js",
 "types": "./dist/index.d.ts",
 "exports": {
 ".": {
 "import": "./dist/index.js",
 "require": "./dist/index.cjs"
 },
 "./utils": "./dist/utils.js"
 },
 "files": [
 "dist/**/*.js",
 "dist/**/*.d.ts",
 "dist/**/*.js.map",
 "README.md",
 "LICENSE"
 ],
 "scripts": {
 "build": "npm run clean && tsc",
 "clean": "rm -rf dist",
 "test": "node --test",
 "prepublishOnly": "npm run build && npm run test"
 }
}

The Type Field

Setting "type": "module" tells Node.js to treat all .js files in your package as ES modules. This is the fundamental setting that enables ESM syntax.

Package Exports

The exports field controls what can be imported from your package:

"exports": {
 ".": {
 "import": "./dist/index.js",
 "require": "./dist/index.cjs",
 "types": "./dist/index.d.ts"
 },
 "./submodule": {
 "import": "./dist/submodule.js",
 "types": "./dist/submodule.d.ts"
 }
}

Files Array

The files array specifies which files to include when publishing. Patterns support glob-like syntax with exclusion support. Properly configuring your package.json is critical for successful npm package distribution. Understanding these configurations helps developers create packages that integrate seamlessly with modern JavaScript tooling and build systems.

Building Your Package

The Build Process

The build process compiles TypeScript source files to JavaScript while generating declaration files and source maps:

# Compile TypeScript
npm run build

# This executes: tsc
# Output in dist/:
# - index.js (compiled JavaScript)
# - index.d.ts (type declarations)
# - index.js.map (source map)

Each TypeScript file generates three artifacts:

  1. .js file: The compiled JavaScript code using ESM syntax
  2. .d.ts file: Type declarations describing the public API
  3. .js.map file: Source map linking back to TypeScript source

Dry-Run Testing

Before publishing, use dry-run to verify what will be included:

# See what would be published
npm pack --dry-run
npm publish --dry-run

Watch Mode During Development

{
 "scripts": {
 "watch": "tsc --watch"
 }
}

Publishing to npm

Preparing for Publication

  1. Create an npm account at npmjs.com
  2. Enable two-factor authentication for security
  3. Configure your package name following npm naming conventions
  4. Write a comprehensive README.md explaining usage and API

Scoped Packages

For organization-owned packages, use scoped names:

{ "name": "@your-org/my-package" }

Scoped packages default to private. To publish publicly:

npm publish --access public

Publishing Process

cd my-package
npm run build
npm test
npm publish

For subsequent versions, use semantic versioning:

npm version patch # Bug fixes
npm version minor # New features
npm version major # Breaking changes

Automated build and deployment pipelines are essential for maintaining quality npm packages. Our CI/CD implementation services can help set up automated testing and publishing workflows that ensure consistent package quality and reliable distribution to consumers.

Type Declarations and Declaration Maps

Generating Type Declarations

TypeScript's declaration generation creates .d.ts files that describe your package's API to TypeScript consumers:

// Your source code
export interface User {
 id: string;
 name: string;
}

export function createUser(name: string): User;
// Generated declaration file
export interface User {
 id: string;
 name: string;
}
declare function createUser(name: string): User;

Declaration Maps for Better IDE Experience

Setting "declarationMap": true creates .d.ts.map files that link declaration files back to TypeScript sources:

{
 "compilerOptions": {
 "declaration": true,
 "declarationMap": true
 }
}

Bundling Declarations

Include declaration files in your package's files array:

{
 "files": [
 "dist/**/*.js",
 "dist/**/*.d.ts",
 "dist/**/*.js.map"
 ]
}

Type declarations are crucial for providing an excellent developer experience when others use your package. Learn more about TypeScript best practices for enterprise applications and how proper type definition can improve code maintainability and developer productivity across large codebases.

Security Best Practices

Account Security

  • Enable two-factor authentication on your npm account before publishing
  • Use a strong, unique password or passkey
  • Review and rotate authentication tokens periodically

Package Security

  • Use tools like Snyk to scan for vulnerabilities in dependencies
  • Keep dependencies updated to receive security patches
  • Review your package's code for potential security issues

Token Management

For automated publishing via CI/CD, use npm access tokens:

echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc

Testing Your Package

Unit Testing with Node.js Test Runner

{
 "scripts": {
 "test": "node --experimental-strip-types test/*.test.ts"
 }
}

Writing Tests

import { describe, it } from 'node:test';
import assert from 'node:assert';
import { myFunction } from '../src/index.js';

describe('myFunction', () => {
 it('should return expected result', () => {
 const result = myFunction('input');
 assert.strictEqual(result, 'expected');
 });
});

Security and testing are foundational to reliable software. Explore our comprehensive web security services for guidance on securing your Node.js applications and npm packages. Implementing robust security practices not only protects your package users but also contributes to better SEO performance as search engines prioritize secure, well-maintained software.

CI/CD and Automated Publishing

GitHub Actions Example

name: Publish Package

on:
 release:
 types: [created]

jobs:
 publish:
 runs-on: ubuntu-latest
 permissions:
 contents: read
 id-token: write
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: '20'
 registry-url: 'https://registry.npmjs.org'
 - run: npm ci
 - run: npm run build
 - run: npm test
 - run: npm publish
 env:
 NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Automated Version Management

Use changesets for managing versioning:

npm install -D @changesets/cli
npx changeset init
npx changeset # Create a changeset
npx changeset version # Version and publish

Automated CI/CD pipelines ensure consistent package publishing. Our team specializes in setting up automated deployment workflows that include testing, security scanning, and reliable npm package distribution. Integrating automated pipelines with AI-powered development workflows can further enhance efficiency and reduce manual errors in the publishing process.

Performance Considerations

Tree Shaking Optimization

Structure exports to enable effective tree shaking:

// Good: Named exports enable tree shaking
export function usedFunction() { /* ... */ }
export function anotherUsed() { /* ... */ }
export const unused = 'value'; // Can be eliminated

Module Resolution Efficiency

{
 "compilerOptions": {
 "moduleResolution": "NodeNext"
 }
}

This setting ensures TypeScript follows Node.js's native ESM resolution.

Bundle Size Considerations

  • Avoid large dependencies when smaller alternatives exist
  • Use ES modules to enable tree shaking
  • Consider dual packaging (ESM + CommonJS) for maximum compatibility

Performance optimization is a core aspect of modern JavaScript development. Well-optimized npm packages contribute to faster application builds and smaller bundle sizes for end users. Additionally, website performance directly impacts search engine rankings, making performance optimization a critical consideration for any web development project.

Essential Tools for npm Package Development

publint

Verify package configuration and identify common issues before publishing.

arethetypeswrong

Check type declarations for issues that could cause problems for consumers.

Knip

Detect unused dependencies, files, and exports to keep packages lean.

Snyk

Scan dependencies for security vulnerabilities before publishing.

Common Pitfalls and Solutions

Problem: Module Resolution Failures

Symptom: "Cannot find module" errors when importing your package

Solution: Ensure package.json has correct exports field and TypeScript has NodeNext module resolution.

Problem: Type Declarations Not Found

Symptom: TypeScript can't find types for your package

Solution: Verify the types field points to your declaration file and declaration files are included in the published package.

Problem: Circular Dependencies

Symptom: Runtime errors or unexpected behavior

Solution: Restructure code to eliminate circular imports or use dynamic imports for deferred resolution.

Problem: Mixed ESM and CommonJS

Symptom: Import/require errors in certain environments

Solution: Decide on ESM-first approach and configure build process accordingly. Consider conditional exports if dual support is needed.

Troubleshooting npm package issues requires understanding both TypeScript compilation and Node.js module systems. Our development team has extensive experience resolving complex module resolution and type declaration challenges, helping teams overcome common hurdles in npm package development and distribution.

Conclusion

Publishing npm packages with TypeScript and ES Modules is straightforward with modern tooling. Focus on:

  1. Proper project structure that separates source from build output
  2. Correct TypeScript configuration using NodeNext module settings
  3. Complete package.json with type field, exports, and files array
  4. Comprehensive testing before publication
  5. Security best practices including 2FA and dependency scanning
  6. Automated CI/CD for consistent, reliable publishing

The key is leveraging modern TypeScript (4.7+) and Node.js (18+) features that provide native ESM support, eliminating much of the complexity that characterized earlier approaches. Your packages will be more compatible, more performant, and easier for consumers to use.

Ready to build and publish your own npm packages? Our web development team specializes in modern JavaScript tooling, TypeScript configuration, and automated deployment pipelines that ensure your packages are production-ready. We can help you navigate the complexities of npm package publishing and establish robust development practices that scale with your project's needs.

Frequently Asked Questions

Ready to Build Your Next npm Package?

Our team specializes in modern JavaScript development including npm package creation, TypeScript configuration, and CI/CD automation.