Agents lang-typescript-library-dev
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.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/lang-typescript-library-dev" ~/.claude/skills/arustydev-agents-lang-typescript-library-dev && rm -rf "$T"
content/skills/lang-typescript-library-dev/SKILL.mdTypeScript 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
- Foundational library patterns (API design, versioning, testing strategies)meta-library-dev
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 | or |
| Build | or bundler command |
| Test | or |
| Lint | |
| Format | |
| Pack (dry run) | |
| Publish | |
| Publish (scoped 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 |
|---|---|
| Generate files |
| Enable "Go to Definition" in source |
| Only emit declarations (use with bundler) |
| Separate output for declarations |
Module Systems
| Config | Output | Use Case |
|---|---|---|
| ESM with | Modern Node.js packages |
| CJS with | Legacy Node.js |
| 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
for type-only exports:export type
// 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
-
succeedsnpm run build -
passesnpm run test -
passesnpm run lint - Version bumped in package.json
- CHANGELOG.md updated
- README.md is current
-
shows correct filesnpm pack --dry-run - 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 field | Add to package.json |
| Wrong export order | Put first in exports conditions |
| Declaration files not generated | Set in tsconfig.json |
| Files not published | Check field includes |
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
keyword (not justexport
)module.exports - Check
in tsconfig.json covers all source filesinclude - Verify no
hiding type errors// @ts-ignore
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 setting matches desired output |
| Source maps not working | Enable and |
| Test files in dist | Add test patterns to in tsconfig |
| node_modules in output | Ensure is set to |
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
- Foundational library patternsmeta-library-dev
- TypeScript syntax and patternslang-typescript-patterns-dev- TypeScript Handbook: Publishing
- npm Docs: package.json
- Are The Types Wrong? - Validate package types