Claude Code Plugins

Community-maintained marketplace

Feedback

lang-typescript-library-dev

@aRustyDev/ai
0
0

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 Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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