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:
{
"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:
{
"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
{
"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):
{
"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
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist/cjs"
}
}
tsconfig.esm.json
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "es2020",
"outDir": "./dist/esm"
}
}
Then update your scripts:
"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
:
/**
* 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
:
/**
* 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
:
// Re-export everything
export * from './arithmetic';
export * from './statistics';
3. Configure tsconfig.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
{
"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
npm run build
npm publish
6. How to use the published package
For TypeScript users:
import { add, average } from 'math-utils';
const sum = add(5, 3); // 8
const avg = average([1, 2, 3, 4, 5]); // 3
For JavaScript users:
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:
{
"compilerOptions": {
// ...other options
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"],
"@models/*": ["src/models/*"]
}
}
}
Then in your code:
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:
{
"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:
- Install API Extractor:
npm install --save-dev @microsoft/api-extractor
- Create a configuration file (
api-extractor.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 }
}
- Add to your build script:
{
"scripts": {
"build": "tsc && api-extractor run --local"
}
}
Package Configuration Best Practices
- Peer Dependencies: List compatible frameworks or libraries that your package works with as peer dependencies
{
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
}
}
}
- Expose Package Version: Make your package version accessible programmatically
// src/version.ts
export const version: string = '__VERSION__';
// During build, replace '__VERSION__' with actual version from package.json
- Include Source Maps: Always include source maps to help with debugging
{
"compilerOptions": {
"sourceMap": true
}
}
- Provide Documentation: Include JSDoc comments for better IDE integration
/**
* 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:
- Use
npm link
:
# In your package directory
npm run build
npm link
# In your test project directory
npm link my-package-name
- Use
npm pack
:
# 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
, andtypes
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:
{
"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:
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:
- Create a simple utility library with dual module support (CJS and ESM)
- Add path aliases to a TypeScript project and ensure they work correctly when published
- Create a package that exports both TypeScript types and JavaScript code
- 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! :)