| name | lang-typescript-library-dev |
| description | TypeScript-specific library/package development patterns. Use when creating npm packages, configuring package.json exports, setting up tsconfig.json for libraries, generating declaration files, publishing to npm, or configuring ESM/CJS dual packages. Extends meta-library-dev with TypeScript tooling and ecosystem patterns. |
TypeScript Library Development
TypeScript-specific patterns for library/package development. This skill extends meta-library-dev with TypeScript tooling, module system configuration, and npm ecosystem practices.
This Skill Extends
meta-library-dev- Foundational library patterns (API design, versioning, testing strategies)
For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first.
This Skill Adds
- TypeScript tooling: tsconfig.json for libraries, declaration files, source maps
- Package configuration: package.json exports, ESM/CJS dual packages, bundling
- npm ecosystem: Publishing workflow, scoped packages, monorepos
This Skill Does NOT Cover
- General library patterns - see
meta-library-dev - TypeScript syntax/patterns - see
lang-typescript-patterns-dev - React component libraries - see frontend skills
- Node.js application development
Overview
Publishing a TypeScript library requires careful configuration of multiple interconnected systems:
┌─────────────────────────────────────────────────────────────────┐
│ TypeScript Library Stack │
├─────────────────────────────────────────────────────────────────┤
│ Source Code (src/) │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ tsconfig │───▶│ TypeScript │───▶│ Declaration │ │
│ │ .json │ │ Compiler │ │ Files (.d.ts)│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ JavaScript │ │ │
│ │ │ Output │ │ │
│ │ └─────────────┘ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ package.json │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ exports │ │ main │ │ types │ │ │
│ │ │ field │ │ module │ │ field │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ npm │ │
│ │ publish │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
Key Decision Points:
| Decision | Options | Recommendation |
|---|---|---|
| Module format | ESM-only, CJS-only, Dual | ESM-only for new packages; Dual if supporting legacy |
| Build tool | tsc, tsup, unbuild, rollup | tsup for simplicity; tsc for control |
| Declaration files | Inline, Separate dir | Inline (same dir as JS) |
| Monorepo tool | pnpm workspaces, turborepo, nx | pnpm workspaces for simplicity |
Quick Reference
| Task | Command |
|---|---|
| New package | npm init or pnpm init |
| Build | tsc or bundler command |
| Test | vitest or jest |
| Lint | eslint . |
| Format | prettier --write . |
| Pack (dry run) | npm pack --dry-run |
| Publish | npm publish |
| Publish (scoped public) | npm publish --access public |
Package.json Structure
Required Fields for Publishing
{
"name": "my-library",
"version": "1.0.0",
"description": "A brief description of what this library does",
"license": "MIT",
"author": "Your Name <email@example.com>",
"repository": {
"type": "git",
"url": "https://github.com/username/repo"
},
"keywords": ["keyword1", "keyword2", "keyword3"],
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"engines": {
"node": ">=18.0.0"
}
}
Exports Field (Modern)
The exports field controls what can be imported:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.js",
"require": "./dist/utils.cjs"
},
"./package.json": "./package.json"
}
}
Order matters: types must come first for TypeScript resolution.
Files Field
Control what gets published:
{
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
]
}
Always verify with npm pack --dry-run.
tsconfig.json for Libraries
Base Configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
Declaration Files
| Option | Purpose |
|---|---|
declaration: true |
Generate .d.ts files |
declarationMap: true |
Enable "Go to Definition" in source |
emitDeclarationOnly: true |
Only emit declarations (use with bundler) |
declarationDir |
Separate output for declarations |
Module Systems
| Config | Output | Use Case |
|---|---|---|
"module": "NodeNext" |
ESM with .js |
Modern Node.js packages |
"module": "CommonJS" |
CJS with .js |
Legacy Node.js |
"module": "ESNext" |
ESM | For bundlers |
ESM/CJS Dual Package
Strategy 1: Dual Build (Recommended)
Build both formats from TypeScript:
{
"scripts": {
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p tsconfig.esm.json",
"build:cjs": "tsc -p tsconfig.cjs.json"
}
}
tsconfig.esm.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "NodeNext",
"outDir": "./dist/esm"
}
}
tsconfig.cjs.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist/cjs"
}
}
Strategy 2: Use a Bundler
Use tsup, unbuild, or rollup for simpler dual builds:
tsup.config.ts:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
dts: true,
clean: true,
sourcemap: true,
});
package.json scripts:
{
"scripts": {
"build": "tsup"
}
}
Strategy 3: ESM-Only (Simplest)
For modern packages, consider ESM-only:
{
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}
Public API Design
Export Patterns
Explicit Named Exports (Preferred):
// src/index.ts
export { parse, serialize } from './parser.js';
export { validate } from './validator.js';
export type { Config, Options, Result } from './types.js';
Avoid Default Exports:
// Avoid: Harder to tree-shake, inconsistent naming
export default class Parser { }
// Prefer: Named exports
export class Parser { }
Type Exports
Use export type for type-only exports:
// Enables proper tree-shaking and prevents runtime import
export type { User, Config } from './types.js';
// Re-export with types
export { parseUser, type ParseOptions } from './parser.js';
Barrel Files
src/index.ts (public API):
// Public API - explicit exports
export { createClient } from './client.js';
export { parse, serialize } from './parser.js';
export type { ClientOptions, ParseResult } from './types.js';
// Do NOT re-export internal modules
// import './internal.js'; // Wrong
Type Declaration Best Practices
Provide Good Types
// Good: Specific, useful types
export interface ClientOptions {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
}
export function createClient(options: ClientOptions): Client;
// Avoid: Overly generic
export function createClient(options: object): unknown;
Use Generics Appropriately
// Good: Generic with constraints
export function parse<T extends Record<string, unknown>>(
input: string,
schema: Schema<T>
): T;
// Good: Infer return type
export function map<T, U>(
items: T[],
fn: (item: T) => U
): U[];
Document with JSDoc
/**
* Parses a configuration string into a typed object.
*
* @param input - The configuration string to parse
* @param options - Optional parsing options
* @returns The parsed configuration object
* @throws {ParseError} If the input is malformed
*
* @example
* ```typescript
* const config = parse('key=value', { strict: true });
* console.log(config.key); // 'value'
* ```
*/
export function parse<T>(input: string, options?: ParseOptions): T;
Testing Libraries
Vitest Configuration
vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['**/*.test.ts', '**/*.d.ts'],
},
},
});
Test File Organization
src/
├── parser.ts
├── parser.test.ts # Unit tests next to source
├── validator.ts
├── validator.test.ts
└── __tests__/ # Or separate test directory
└── integration.test.ts
Type Testing
Test that types work correctly:
import { expectTypeOf } from 'vitest';
import { parse } from './parser.js';
test('parse returns correct type', () => {
const result = parse('{"name": "test"}');
expectTypeOf(result).toEqualTypeOf<ParsedResult>();
});
Monorepo Patterns
pnpm Workspace
pnpm-workspace.yaml:
packages:
- 'packages/*'
Package Structure
my-monorepo/
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.json # Base config
└── packages/
├── core/
│ ├── package.json
│ ├── tsconfig.json # Extends base
│ └── src/
└── utils/
├── package.json
├── tsconfig.json
└── src/
Internal Dependencies
{
"name": "@myorg/app",
"dependencies": {
"@myorg/core": "workspace:*",
"@myorg/utils": "workspace:*"
}
}
Project References
Root tsconfig.json:
{
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/utils" }
]
}
Package tsconfig.json:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../utils" }
]
}
Publishing to npm
Pre-publish Checklist
-
npm run buildsucceeds -
npm run testpasses -
npm run lintpasses - Version bumped in package.json
- CHANGELOG.md updated
- README.md is current
-
npm pack --dry-runshows correct files - Types are correctly generated
- Exports work:
node -e "import('my-lib')"
Publishing Commands
# Verify package contents
npm pack --dry-run
# Publish to npm
npm publish
# Publish scoped package as public
npm publish --access public
# Publish with tag (for pre-releases)
npm publish --tag beta
Scoped Packages
{
"name": "@myorg/my-library",
"publishConfig": {
"access": "public"
}
}
Automation with Changesets
# Initialize changesets
npx changeset init
# Add a changeset
npx changeset
# Version packages
npx changeset version
# Publish
npx changeset publish
Common Dependencies
Build Tools
{
"devDependencies": {
"typescript": "^5.0.0",
"tsup": "^8.0.0",
"@types/node": "^20.0.0"
}
}
Testing
{
"devDependencies": {
"vitest": "^1.0.0",
"@vitest/coverage-v8": "^1.0.0"
}
}
Linting/Formatting
{
"devDependencies": {
"eslint": "^8.0.0",
"typescript-eslint": "^7.0.0",
"prettier": "^3.0.0"
}
}
Anti-Patterns
1. Missing Types Field
// Bad: Types not specified
{
"main": "./dist/index.js"
}
// Good: Types explicitly declared
{
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
2. Wrong Export Order
// Bad: types not first
{
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}
// Good: types first
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
3. Publishing Source Files
// Bad: Publishing everything
{
"files": ["src", "dist"]
}
// Good: Only publish dist
{
"files": ["dist"]
}
4. Missing Peer Dependencies
// Bad: Bundling React in a React library
{
"dependencies": {
"react": "^18.0.0"
}
}
// Good: Peer dependency
{
"peerDependencies": {
"react": "^18.0.0"
}
}
Troubleshooting
Types Not Found by Consumers
Symptom: Cannot find module 'my-lib' or its corresponding type declarations
Causes & Fixes:
| Cause | Fix |
|---|---|
Missing types field |
Add "types": "./dist/index.d.ts" to package.json |
| Wrong export order | Put types first in exports conditions |
| Declaration files not generated | Set "declaration": true in tsconfig.json |
| Files not published | Check files field includes dist |
Diagnostic:
# Check what's actually published
npm pack --dry-run
# Validate types configuration
npx @arethetypeswrong/cli my-package
ESM/CJS Import Errors
Symptom: ERR_REQUIRE_ESM or Must use import to load ES Module
Common Fixes:
// Ensure package.json has correct type
{
"type": "module" // For ESM-first packages
}
// Or provide both formats in exports
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
Declaration Files Missing Exports
Symptom: Types exist but some exports show as any
Fixes:
- Ensure all exports use
exportkeyword (not justmodule.exports) - Check
includein tsconfig.json covers all source files - Verify no
// @ts-ignorehiding type errors
Monorepo Package Resolution
Symptom: Cannot find module '@myorg/shared' in monorepo
Fixes:
// tsconfig.json - Add path mapping
{
"compilerOptions": {
"paths": {
"@myorg/*": ["./packages/*/src"]
}
}
}
// Or use TypeScript project references
{
"references": [
{ "path": "../shared" }
]
}
Build Output Issues
| Problem | Solution |
|---|---|
| Output files have wrong extension | Check module setting matches desired output |
| Source maps not working | Enable sourceMap and declarationMap |
| Test files in dist | Add test patterns to exclude in tsconfig |
| node_modules in output | Ensure rootDir is set to ./src |
Publishing Failures
Pre-publish checklist:
# 1. Verify package contents
npm pack --dry-run
# 2. Test local install
npm pack && npm install ./my-package-1.0.0.tgz
# 3. Test imports work
node -e "import('my-package').then(console.log)"
# 4. Check for accidental secrets
grep -r "api_key\|password\|secret" dist/
References
meta-library-dev- Foundational library patternslang-typescript-patterns-dev- TypeScript syntax and patterns- TypeScript Handbook: Publishing
- npm Docs: package.json
- Are The Types Wrong? - Validate package types