Agent Skills: TypeScript Library Development

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.

UncategorizedID: arustydev/ai/lang-typescript-library-dev

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/lang-typescript-library-dev

Skill Files

Browse the full folder contents for lang-typescript-library-dev.

Download Skill

Loading file tree…

content/skills/lang-typescript-library-dev/SKILL.md

Skill Metadata

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 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:

| 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:

  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

| 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