AgentSkillsCN

lang-typescript-library-dev

TypeScript 特定的库/包开发模式。当需要创建 npm 包、配置 package.json 的导出项、为库设置 tsconfig.json、生成声明文件、发布至 npm,或配置 ESM/CJS 双重包时,可使用此技能。该技能在 meta-library-dev 的基础上,新增了 TypeScript 工具链与生态系统的相关模式。

SKILL.md
--- frontmatter
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:

code
┌─────────────────────────────────────────────────────────────────┐
│                    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 packagenpm init or pnpm init
Buildtsc or bundler command
Testvitest or jest
Linteslint .
Formatprettier --write .
Pack (dry run)npm pack --dry-run
Publishnpm publish
Publish (scoped public)npm publish --access public

Package.json Structure

Required Fields for Publishing

json
{
  "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:

json
{
  "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:

json
{
  "files": [
    "dist",
    "!dist/**/*.test.*",
    "!dist/**/*.spec.*"
  ]
}

Always verify with npm pack --dry-run.


tsconfig.json for Libraries

Base Configuration

json
{
  "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: trueGenerate .d.ts files
declarationMap: trueEnable "Go to Definition" in source
emitDeclarationOnly: trueOnly emit declarations (use with bundler)
declarationDirSeparate output for declarations

Module Systems

ConfigOutputUse Case
"module": "NodeNext"ESM with .jsModern Node.js packages
"module": "CommonJS"CJS with .jsLegacy Node.js
"module": "ESNext"ESMFor bundlers

ESM/CJS Dual Package

Strategy 1: Dual Build (Recommended)

Build both formats from TypeScript:

json
{
  "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:

json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "NodeNext",
    "outDir": "./dist/esm"
  }
}

tsconfig.cjs.json:

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:

typescript
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  dts: true,
  clean: true,
  sourcemap: true,
});

package.json scripts:

json
{
  "scripts": {
    "build": "tsup"
  }
}

Strategy 3: ESM-Only (Simplest)

For modern packages, consider ESM-only:

json
{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

Public API Design

Export Patterns

Explicit Named Exports (Preferred):

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

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

typescript
// 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):

typescript
// 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

typescript
// 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

typescript
// 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

typescript
/**
 * 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:

typescript
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

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

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

yaml
packages:
  - 'packages/*'

Package Structure

code
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

json
{
  "name": "@myorg/app",
  "dependencies": {
    "@myorg/core": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

Project References

Root tsconfig.json:

json
{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" }
  ]
}

Package tsconfig.json:

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

bash
# 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

json
{
  "name": "@myorg/my-library",
  "publishConfig": {
    "access": "public"
  }
}

Automation with Changesets

bash
# Initialize changesets
npx changeset init

# Add a changeset
npx changeset

# Version packages
npx changeset version

# Publish
npx changeset publish

Common Dependencies

Build Tools

json
{
  "devDependencies": {
    "typescript": "^5.0.0",
    "tsup": "^8.0.0",
    "@types/node": "^20.0.0"
  }
}

Testing

json
{
  "devDependencies": {
    "vitest": "^1.0.0",
    "@vitest/coverage-v8": "^1.0.0"
  }
}

Linting/Formatting

json
{
  "devDependencies": {
    "eslint": "^8.0.0",
    "typescript-eslint": "^7.0.0",
    "prettier": "^3.0.0"
  }
}

Anti-Patterns

1. Missing Types Field

json
// 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

json
// 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

json
// Bad: Publishing everything
{
  "files": ["src", "dist"]
}

// Good: Only publish dist
{
  "files": ["dist"]
}

4. Missing Peer Dependencies

json
// 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 fieldAdd "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:

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

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

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

bash
# 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