Skip to main content

TypeScript Package Configuration

Introduction

When developing TypeScript projects that you intend to share with others or publish as packages to npm, proper configuration is essential. Package configuration involves setting up your TypeScript project so that it can be easily consumed by other developers, whether they're using TypeScript or JavaScript.

In this guide, we'll explore how to configure TypeScript packages for distribution, covering everything from basic setup to advanced techniques that ensure your packages are reliable, easy to use, and maintainable.

Basic Package Structure

A well-organized TypeScript package typically follows this structure:

my-package/
├── src/
│ ├── index.ts
│ └── (other source files)
├── dist/
│ ├── index.js
│ ├── index.d.ts
│ └── (other compiled files)
├── tsconfig.json
├── package.json
├── README.md
└── LICENSE
  • src/ - Contains all your TypeScript source code
  • dist/ - Where compiled JavaScript and declaration files will be output
  • tsconfig.json - TypeScript configuration
  • package.json - npm package configuration

Setting Up tsconfig.json

The tsconfig.json file is crucial for TypeScript packages as it determines how your code is compiled.

Here's a recommended configuration for packages:

json
{
"compilerOptions": {
"target": "es2018", // Target ECMAScript version
"module": "commonjs", // Module system to use
"declaration": true, // Generate .d.ts files
"outDir": "./dist", // Output directory
"strict": true, // Enable all strict type checking options
"esModuleInterop": true, // Better import/export compatibility
"skipLibCheck": true, // Skip type checking of declaration files
"forceConsistentCasingInFileNames": true,
"removeComments": false, // Keep comments in output
"sourceMap": true, // Generate source maps
"rootDir": "./src" // Source directory
},
"include": ["src/**/*"], // Which files to compile
"exclude": ["node_modules", "**/*.test.ts"] // What to exclude
}

Key Options Explained

  • declaration: Set to true to generate .d.ts declaration files, which provide type information to consumers of your package
  • outDir: Specifies where the compiled JavaScript goes
  • target: The ECMAScript version to compile to
  • module: The module system (CommonJS for Node.js compatibility, or ESM for modern environments)

Configuring package.json

Your package.json file needs special configuration for TypeScript packages:

json
{
"name": "my-typescript-package",
"version": "1.0.0",
"description": "A fantastic TypeScript package",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": ["typescript", "example"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"typescript": "^4.7.4"
}
}

Key Fields Explained

  • main: Points to the compiled JavaScript entry file
  • types or typings: Points to the main declaration file
  • files: Specifies which files should be included when the package is published
  • scripts: Build and prepublish scripts
  • prepublishOnly: Ensures the package is built before publishing

Dual Package Support (CommonJS and ES Modules)

Modern TypeScript packages often support both CommonJS (traditional Node.js) and ES Modules. Here's how to configure this:

Updated package.json

json
{
"name": "my-typescript-package",
"version": "1.0.0",
"description": "A package with dual module support",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
"files": [
"dist/**/*"
],
"scripts": {
"build": "npm run build:cjs && npm run build:esm",
"build:cjs": "tsc --outDir dist/cjs --module commonjs",
"build:esm": "tsc --outDir dist/esm --module es2020",
"prepublishOnly": "npm run build"
},
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
}
}
}

Multiple tsconfig Files

To support dual builds, you might use multiple tsconfig files:

tsconfig.json (base configuration):

json
{
"compilerOptions": {
"target": "es2018",
"declaration": true,
"declarationDir": "dist/types",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}

tsconfig.cjs.json:

json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist/cjs"
}
}

tsconfig.esm.json:

json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "es2020",
"outDir": "./dist/esm"
}
}

Then update your scripts:

json
"scripts": {
"build": "npm run build:types && npm run build:cjs && npm run build:esm",
"build:types": "tsc --declaration --emitDeclarationOnly --outDir dist/types",
"build:cjs": "tsc --project tsconfig.cjs.json",
"build:esm": "tsc --project tsconfig.esm.json"
}

Real-World Example: Creating a Utility Library

Let's walk through creating a simple utility library with proper package configuration:

1. Create the project structure

math-utils/
├── src/
│ ├── index.ts
│ ├── arithmetic.ts
│ └── statistics.ts
├── tsconfig.json
└── package.json

2. Implement the functionality

src/arithmetic.ts:

typescript
/**
* Adds two numbers together
*/
export function add(a: number, b: number): number {
return a + b;
}

/**
* Multiplies two numbers
*/
export function multiply(a: number, b: number): number {
return a * b;
}

src/statistics.ts:

typescript
/**
* Calculates the average of an array of numbers
*/
export function average(numbers: number[]): number {
if (numbers.length === 0) return 0;
const sum = numbers.reduce((acc, val) => acc + val, 0);
return sum / numbers.length;
}

/**
* Finds the maximum value in an array of numbers
*/
export function max(numbers: number[]): number | undefined {
if (numbers.length === 0) return undefined;
return Math.max(...numbers);
}

src/index.ts:

typescript
// Re-export everything
export * from './arithmetic';
export * from './statistics';

3. Configure tsconfig.json

json
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "**/*.test.ts"]
}

4. Configure package.json

json
{
"name": "math-utils",
"version": "1.0.0",
"description": "Simple math utility functions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": ["math", "utilities", "typescript"],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"typescript": "^4.7.4"
}
}

5. Build and publish

bash
npm run build
npm publish

6. How to use the published package

For TypeScript users:

typescript
import { add, average } from 'math-utils';

const sum = add(5, 3); // 8
const avg = average([1, 2, 3, 4, 5]); // 3

For JavaScript users:

javascript
const { multiply, max } = require('math-utils');

const product = multiply(4, 5); // 20
const highest = max([10, 7, 25, 13]); // 25

Advanced Package Configuration

Path Aliases

You can use path aliases to create cleaner imports in your code:

json
{
"compilerOptions": {
// ...other options
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"],
"@models/*": ["src/models/*"]
}
}
}

Then in your code:

typescript
import { formatDate } from '@utils/date-formatter';
import { User } from '@models/User';

Note: When publishing, consider using a tool like tsc-alias to resolve these paths in the built code.

Declaration Maps

To improve debugging experience for consumers of your library, enable declaration maps:

json
{
"compilerOptions": {
// ...other options
"declaration": true,
"declarationMap": true
}
}

This allows users to navigate to your TypeScript source code directly from their editor.

Bundling with API Extractor

For more complex packages, you might want to generate a single declaration file using Microsoft's API Extractor:

  1. Install API Extractor:
bash
npm install --save-dev @microsoft/api-extractor
  1. Create a configuration file (api-extractor.json):
json
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/my-package.d.ts"
},
"apiReport": { "enabled": false },
"docModel": { "enabled": false },
"tsdocMetadata": { "enabled": false }
}
  1. Add to your build script:
json
{
"scripts": {
"build": "tsc && api-extractor run --local"
}
}

Package Configuration Best Practices

  1. Peer Dependencies: List compatible frameworks or libraries that your package works with as peer dependencies
json
{
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
}
  1. Expose Package Version: Make your package version accessible programmatically
typescript
// src/version.ts
export const version: string = '__VERSION__';

// During build, replace '__VERSION__' with actual version from package.json
  1. Include Source Maps: Always include source maps to help with debugging
json
{
"compilerOptions": {
"sourceMap": true
}
}
  1. Provide Documentation: Include JSDoc comments for better IDE integration
typescript
/**
* Formats a date according to the specified format string
* @param date - The date to format
* @param format - The format string
* @returns The formatted date string
* @example
* ```ts
* formatDate(new Date(), 'YYYY-MM-DD')
* // Returns: '2023-05-22'
* ```
*/
export function formatDate(date: Date, format: string): string {
// Implementation
}

Testing Your Package Locally

Before publishing, test your package locally:

  1. Use npm link:
bash
# In your package directory
npm run build
npm link

# In your test project directory
npm link my-package-name
  1. Use npm pack:
bash
# In your package directory
npm run build
npm pack # Creates a .tgz file

# In your test project
npm install ../path/to/my-package-1.0.0.tgz

Common Issues and Solutions

1. "Module not found" errors

If consumers get "module not found" errors, check:

  • That your main, module, and types fields in package.json point to existing files
  • That the files field includes all necessary directories
  • That imports in your code match the case of actual filenames (important for case-sensitive systems)

2. TypeScript version mismatches

If you use advanced TypeScript features, specify the minimum TypeScript version:

json
{
"peerDependencies": {
"typescript": ">=4.5.0"
},
"devDependencies": {
"typescript": "^4.7.4"
}
}

3. Node.js vs browser environment

For packages that work in multiple environments, use environment checks:

typescript
export function readFile(path: string): string {
if (typeof window === 'undefined') {
// Node.js environment
const fs = require('fs');
return fs.readFileSync(path, 'utf8');
} else {
// Browser environment
throw new Error('readFile is not supported in browser environments');
}
}

Summary

Configuring TypeScript packages properly ensures that your code is usable, maintainable, and reliable for both JavaScript and TypeScript consumers. By following the practices outlined in this guide, you can create packages that:

  • Support both CommonJS and ES Modules
  • Provide proper type definitions
  • Work across different environments
  • Follow modern best practices for npm packages

Package configuration may seem complex at first, but once you understand the core concepts, it becomes a powerful tool in creating shareable, high-quality TypeScript libraries.

Additional Resources

Here are some exercises to practice what you've learned:

  1. Create a simple utility library with dual module support (CJS and ESM)
  2. Add path aliases to a TypeScript project and ensure they work correctly when published
  3. Create a package that exports both TypeScript types and JavaScript code
  4. Practice adding proper JSDoc comments to improve your package's documentation

Ready to take your TypeScript modules to the next level? Explore these resources:



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)