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.

install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
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"
manifest: content/skills/lang-typescript-library-dev/SKILL.md
source content

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:

DecisionOptionsRecommendation
Module formatESM-only, CJS-only, DualESM-only for new packages; Dual if supporting legacy
Build tooltsc, tsup, unbuild, rolluptsup for simplicity; tsc for control
Declaration filesInline, Separate dirInline (same dir as JS)
Monorepo toolpnpm workspaces, turborepo, nxpnpm workspaces for simplicity

Quick Reference

TaskCommand
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

OptionPurpose
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

ConfigOutputUse Case
"module": "NodeNext"
ESM with
.js
Modern Node.js packages
"module": "CommonJS"
CJS with
.js
Legacy Node.js
"module": "ESNext"
ESMFor 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 build
    succeeds
  • npm run test
    passes
  • npm run lint
    passes
  • Version bumped in package.json
  • CHANGELOG.md updated
  • README.md is current
  • npm pack --dry-run
    shows 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:

CauseFix
Missing
types
field
Add
"types": "./dist/index.d.ts"
to package.json
Wrong export orderPut
types
first in exports conditions
Declaration files not generatedSet
"declaration": true
in tsconfig.json
Files not publishedCheck
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:

  1. Ensure all exports use
    export
    keyword (not just
    module.exports
    )
  2. Check
    include
    in tsconfig.json covers all source files
  3. Verify no
    // @ts-ignore
    hiding 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

ProblemSolution
Output files have wrong extensionCheck
module
setting matches desired output
Source maps not workingEnable
sourceMap
and
declarationMap
Test files in distAdd test patterns to
exclude
in tsconfig
node_modules in outputEnsure
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