Skip to content

TypeScript Declaration Files (.d.ts) Generation Plan

Status: Planning
Priority: Medium (not blocking, but should be done before public release)
Context: Russian feedback translation + research from TS docs, Bun docs, and reference repos


Executive Summary

Current State: Using types: ./src/index.ts in package.json exports
Target State: Generate proper .d.ts files in dist/ directory
Why Change: Better API control, standard compliance, future-proof for subpaths

The current setup works but isn't the gold standard. It's acceptable during active development but should be fixed before public release.


Problem Analysis

Current Configuration (packages/core/package.json)

json
{
  "exports": {
    ".": {
      "types": "./src/index.ts",  // ⚠️ Points to source
      "bun": "./src/index.ts",
      "default": "./dist/index.js"
    }
  },
  "types": "./dist/index.d.ts"  // ⚠️ Conflicts with exports
}

Issues with Current Approach

  1. Type Surface Leak: TypeScript sees source files directly, potentially exposing internal types not meant to be public
  2. Non-Standard: Most mature libraries use .d.ts files, not source .ts files
  3. Tooling Compatibility: Some tools expect .d.ts files specifically
  4. Future-Proofing: Will cause issues when adding:
    • Subpaths (e.g., @verist/core/testing)
    • stripInternal compiler option
    • Complex type transformations

Why It Works Now

  • Zero external users (active development)
  • Bun-first environment
  • Modern TypeScript with moduleResolution: bundler
  • Reduced friction during rapid iteration

Critical Insight from Feedback

The project emphasizes curated root exports and protecting DX by hiding low-level APIs. The current types: ./src/index.ts approach undermines this philosophy because:

  • TS can still "see" types even if values aren't exported
  • IDE autocomplete may suggest internal types
  • Contradicts the "tight public surface" design goal

Research Findings

TypeScript Official Guidance

From TypeScript Handbook:

  • Recommendation: Bundle declarations with your package
  • Standard Pattern:
    json
    {
      "main": "./lib/main.js",
      "types": "./lib/main.d.ts"
    }
  • For Modern Exports:
    json
    {
      "exports": {
        ".": {
          "types": "./dist/index.d.ts",
          "default": "./dist/index.js"
        }
      }
    }

Bun's Approach

Bun itself uses:

  • @types/bun package with index.d.ts referencing bun-types
  • Standard .d.ts files in published packages
  • Clear separation of runtime and type definitions

Generation Methods

  1. TypeScript Compiler (tsc)

    • Most common, battle-tested
    • Options: declaration: true, emitDeclarationOnly: true
    • Can use stripInternal for private APIs
  2. Bun Build

    • Currently NO native .d.ts generation (as of Bun 1.x)
    • Roadmap item, but not yet available
  3. tsup

    • Wrapper around esbuild with dts generation
    • Popular in modern TS libraries
    • Command: tsup src/index.ts --dts --format esm
  4. API Extractor (Microsoft)

    • Advanced: API reports, documentation, rollup
    • Overkill for current needs

Phase 1: Immediate (Current Development)

Status Quo is Acceptable

  • ✅ Keep types: ./src/index.ts for now
  • ✅ Maintain fast iteration speed
  • ⚠️ Do NOT add subpaths yet
  • ⚠️ Document this as temporary

Rationale: No external users, active refactoring, Bun-native workflow.

Phase 2: Pre-Release (Before v1.0 or Public Announcement)

Switch to Generated .d.ts Files

Pros:

  • Official TypeScript tooling
  • Zero dependencies (already have typescript)
  • Precise control over output
  • Can use stripInternal for hiding private APIs

Cons:

  • Separate build step
  • Slightly slower than bundler-only approach

Implementation:

  1. Update tsconfig.json (root):

    json
    {
      "compilerOptions": {
        "declaration": true,
        "emitDeclarationOnly": false,
        "declarationMap": true,
        "stripInternal": true
      }
    }
  2. Update package tsconfig.json:

    json
    {
      "extends": "../../tsconfig.json",
      "compilerOptions": {
        "rootDir": "src",
        "outDir": "dist",
        "declaration": true,
        "emitDeclarationOnly": true,
        "declarationMap": true
      },
      "include": ["src"]
    }
  3. Update build script (scripts/build.ts):

    typescript
    // After Bun.build(), run:
    await Bun.$`tsc -p ${join(pkgDir, 'tsconfig.json')}`;
  4. Update package.json exports:

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

Option B: Use tsup

Pros:

  • Single tool for bundling + types
  • Popular in modern ecosystem
  • Simpler configuration

Cons:

  • Additional dependency
  • Less control over type generation
  • May conflict with existing Bun build setup

Implementation:

bash
bun add -D tsup
typescript
// Update build.ts to use tsup instead of Bun.build
import { build } from 'tsup';

await build({
  entry: ['src/index.ts'],
  format: ['esm'],
  dts: true,
  sourcemap: true,
  outDir: 'dist',
});

Phase 3: Future Enhancements

Once .d.ts generation is in place:

  1. Add Subpaths:

    json
    "exports": {
      ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
      "./testing": { "types": "./dist/testing.d.ts", "default": "./dist/testing.js" }
    }
  2. API Documentation:

    • Consider API Extractor for API reports
    • Generate markdown docs from JSDoc comments
  3. Type Testing:

    • Add @ts-expect-error tests for type-level behavior
    • Consider tsd or expect-type for type assertions

Implementation Checklist

Pre-Release Tasks

  • [ ] Decision: Choose tsc (recommended) or tsup
  • [ ] Update root tsconfig.json with declaration settings
  • [ ] Update per-package tsconfig.json files
  • [ ] Modify scripts/build.ts to generate .d.ts files
  • [ ] Update all package.json exports to point to ./dist/*.d.ts
  • [ ] Test that types resolve correctly in consuming projects
  • [ ] Verify IDE autocomplete works as expected
  • [ ] Add .d.ts generation to CI/CD pipeline
  • [ ] Update documentation about type exports

Optional Enhancements

  • [ ] Add stripInternal to hide internal APIs
  • [ ] Use @internal JSDoc tags for internal-only exports
  • [ ] Generate API documentation from types
  • [ ] Set up type-testing framework

Migration Strategy

Step-by-Step Rollout

  1. Test in One Package First: Start with @verist/core
  2. Verify Consumer Experience: Test in a separate test project
  3. Roll Out to All Packages: Apply to entire monorepo
  4. Update Documentation: Explain type exports in README

Verification Steps

bash
# 1. Build with types
bun run build

# 2. Check generated files
ls -la packages/core/dist/
# Should see: index.js, index.d.ts, index.d.ts.map

# 3. Test type resolution
mkdir test-consumer && cd test-consumer
bun init -y
bun add ../packages/core
# Create index.ts that imports from @verist/core
# Check that autocomplete and type-checking work

Rollback Plan

If issues arise:

  1. Revert package.json exports to ./src/index.ts
  2. Keep generated .d.ts in dist but don't reference
  3. Debug issue separately without blocking development

Technical Details

Current Build Process

From scripts/build.ts:

typescript
await Bun.build({
  entrypoints,
  outdir: distDir,
  format: "esm",
  target: "node",
  packages: "external",
  sourcemap: "linked",
});

Note: Bun.build() does NOT generate .d.ts files (as of Bun 1.x).

Proposed Addition (Option A: tsc)

typescript
// After successful Bun.build()
if (buildResult.success) {
  // Generate type declarations
  const tscResult = await Bun.$`tsc -p ${join(pkgDir, 'tsconfig.json')}`.quiet();
  
  if (tscResult.exitCode !== 0) {
    console.error(`✗ ${pkg} (tsc failed)`);
    console.error(tscResult.stderr.toString());
    process.exit(1);
  }
}

Root tsconfig.json Changes

Current config has noEmit: true - this is correct for the root config.

Per-package configs should override this:

json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "noEmit": false,  // Override root
    "declaration": true,
    "emitDeclarationOnly": true
  }
}

Performance Considerations

Build Time Impact

  • Current: ~0.5s per package (Bun.build only)
  • With tsc: Estimated +0.3-0.5s per package
  • Total Impact: ~3-5s additional for entire monorepo (10 packages)

Mitigation:

  • Only generate types during bun run build (pre-publish)
  • Dev workflow continues using source files
  • CI/CD pipeline runs full build with types

Development Experience

No Impact on day-to-day development:

  • TypeScript checking already runs via IDE/editor
  • bun run check (tsc in check mode) unchanged
  • Hot reload and testing use source files directly

Standards & Best Practices

Package.json Exports Best Practice

json
{
  "name": "@verist/core",
  "version": "1.0.0",
  "type": "module",
  "sideEffects": false,
  
  // Legacy fields for older tools
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  
  // Modern exports map
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "default": "./dist/index.js"
    },
    "./package.json": "./package.json"
  },
  
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ]
}

Note: Remove src from files array once using .d.ts exclusively.

TypeScript Configuration Best Practice

json
{
  "compilerOptions": {
    // Type generation
    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,
    
    // Strip internal APIs
    "stripInternal": true,
    
    // Output structure
    "rootDir": "src",
    "outDir": "dist",
    
    // Module resolution (already correct)
    "module": "Preserve",
    "moduleResolution": "bundler"
  }
}

FAQ

Q: Why not use Bun.build() for types?

A: Bun doesn't support .d.ts generation yet (as of Bun 1.x). It's on the roadmap but not available.

Q: Should we commit .d.ts files to Git?

A: No. Generate during build/publish. Add to .gitignore:

packages/*/dist/

Q: What about declaration maps?

A: Yes, include them:

  • Helps with "Go to Definition" jumping to source
  • Useful for debugging
  • Minimal size overhead

Q: How do we hide internal APIs?

A: Three approaches:

  1. Don't export from index.ts (already doing this)
  2. Use @internal JSDoc tag
  3. Enable stripInternal compiler option (recommended)

Q: Will this break existing consumers?

A: No, if done correctly:

  • Types remain compatible
  • Only the file path changes (.ts.d.ts)
  • Modern TypeScript handles both

Q: Do we need separate .d.ts for each subpath?

A: Yes, when you add subpaths in the future:

dist/
  index.d.ts       # Main entrypoint
  testing.d.ts     # Testing utilities
  internal.d.ts    # Internal APIs (if exposed)

Confidence Levels

StatementConfidence
Current setup works but isn't optimal0.9
Should switch to .d.ts before v1.00.85
tsc is the best tool for this project0.8
No immediate rush to implement0.85
Will prevent future issues0.9

References


Next Steps

  1. Review this plan with team/stakeholders
  2. Choose timing: Now vs. pre-release
  3. Select tool: tsc (recommended) vs. tsup
  4. Create implementation ticket if approved
  5. Test in isolated branch before merging

Conclusion: The current setup is acceptable for now but should be upgraded to proper .d.ts generation before public release. This change is straightforward, low-risk, and aligns with the project's emphasis on controlled public APIs and professional engineering practices.

LLM context: llms.txt · llms-full.txt
Released under the Apache 2.0 License.