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.
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.
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.
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:
- .js file: The compiled JavaScript code using ESM syntax
- .d.ts file: Type declarations describing the public API
- .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
- Create an npm account at npmjs.com
- Enable two-factor authentication for security
- Configure your package name following npm naming conventions
- 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.
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:
- Proper project structure that separates source from build output
- Correct TypeScript configuration using NodeNext module settings
- Complete package.json with type field, exports, and files array
- Comprehensive testing before publication
- Security best practices including 2FA and dependency scanning
- 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.