| 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":
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":
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):
export { parse, serialize } from './parser.js';
export { validate } from './validator.js';
export type { Config, Options, Result } from './types.js';
Avoid Default Exports:
export default class Parser { }
export class Parser { }
Type Exports
Use export type for type-only exports:
export type { User, Config } from './types.js';
export { parseUser, type ParseOptions } from './parser.js';
Barrel Files
src/index.ts (public API):
export { createClient } from './client.js';
export { parse, serialize } from './parser.js';
export type { ClientOptions, ParseResult } from './types.js';
Type Declaration Best Practices
Provide Good Types
export interface ClientOptions {
baseUrl: string;
timeout?: number;
headers?: Record<string, string>;
}
export function createClient(options: ClientOptions): Client;
export function createClient(options: object): unknown;
Use Generics Appropriately
export function parse<T extends Record<string, unknown>>(
input: string,
schema: Schema<T>
): T;
export function map<T, U>(
items: T[],
fn: (item: T) => U
): U[];
Document with JSDoc
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
Publishing Commands
npm pack --dry-run
npm publish
npm publish --access public
npm publish --tag beta
Scoped Packages
{
"name": "@myorg/my-library",
"publishConfig": {
"access": "public"
}
}
Automation with Changesets
npx changeset init
npx changeset
npx changeset version
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
{
"main": "./dist/index.js"
}
{
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
2. Wrong Export Order
{
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
3. Publishing Source Files
{
"files": ["src", "dist"]
}
{
"files": ["dist"]
}
4. Missing Peer Dependencies
{
"dependencies": {
"react": "^18.0.0"
}
}
{
"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:
npm pack --dry-run
npx @arethetypeswrong/cli my-package
ESM/CJS Import Errors
Symptom: ERR_REQUIRE_ESM or Must use import to load ES Module
Common Fixes:
{
"type": "module"
}
{
"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
export keyword (not just module.exports)
- Check
include in tsconfig.json covers all source files
- Verify no
// @ts-ignore hiding type errors
Monorepo Package Resolution
Symptom: Cannot find module '@myorg/shared' in monorepo
Fixes:
{
"compilerOptions": {
"paths": {
"@myorg/*": ["./packages/*/src"]
}
}
}
{
"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:
npm pack --dry-run
npm pack && npm install ./my-package-1.0.0.tgz
node -e "import('my-package').then(console.log)"
grep -r "api_key\|password\|secret" dist/
References