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)
{
"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
- Type Surface Leak: TypeScript sees source files directly, potentially exposing internal types not meant to be public
- Non-Standard: Most mature libraries use
.d.tsfiles, not source.tsfiles - Tooling Compatibility: Some tools expect
.d.tsfiles specifically - Future-Proofing: Will cause issues when adding:
- Subpaths (e.g.,
@verist/core/testing) stripInternalcompiler option- Complex type transformations
- Subpaths (e.g.,
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/bunpackage withindex.d.tsreferencingbun-types- Standard
.d.tsfiles in published packages - Clear separation of runtime and type definitions
Generation Methods
TypeScript Compiler (tsc)
- Most common, battle-tested
- Options:
declaration: true,emitDeclarationOnly: true - Can use
stripInternalfor private APIs
Bun Build
- Currently NO native
.d.tsgeneration (as of Bun 1.x) - Roadmap item, but not yet available
- Currently NO native
tsup
- Wrapper around esbuild with dts generation
- Popular in modern TS libraries
- Command:
tsup src/index.ts --dts --format esm
API Extractor (Microsoft)
- Advanced: API reports, documentation, rollup
- Overkill for current needs
Recommended Solution
Phase 1: Immediate (Current Development)
Status Quo is Acceptable
- ✅ Keep
types: ./src/index.tsfor 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
Option A: Use tsc (Recommended)
Pros:
- Official TypeScript tooling
- Zero dependencies (already have typescript)
- Precise control over output
- Can use
stripInternalfor hiding private APIs
Cons:
- Separate build step
- Slightly slower than bundler-only approach
Implementation:
Update tsconfig.json (root):
json{ "compilerOptions": { "declaration": true, "emitDeclarationOnly": false, "declarationMap": true, "stripInternal": true } }Update package tsconfig.json:
json{ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "src", "outDir": "dist", "declaration": true, "emitDeclarationOnly": true, "declarationMap": true }, "include": ["src"] }Update build script (scripts/build.ts):
typescript// After Bun.build(), run: await Bun.$`tsc -p ${join(pkgDir, 'tsconfig.json')}`;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:
bun add -D tsup// 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:
Add Subpaths:
json"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./testing": { "types": "./dist/testing.d.ts", "default": "./dist/testing.js" } }API Documentation:
- Consider API Extractor for API reports
- Generate markdown docs from JSDoc comments
Type Testing:
- Add
@ts-expect-errortests for type-level behavior - Consider
tsdorexpect-typefor type assertions
- Add
Implementation Checklist
Pre-Release Tasks
- [ ] Decision: Choose tsc (recommended) or tsup
- [ ] Update root
tsconfig.jsonwith declaration settings - [ ] Update per-package
tsconfig.jsonfiles - [ ] Modify
scripts/build.tsto generate.d.tsfiles - [ ] Update all
package.jsonexports to point to./dist/*.d.ts - [ ] Test that types resolve correctly in consuming projects
- [ ] Verify IDE autocomplete works as expected
- [ ] Add
.d.tsgeneration to CI/CD pipeline - [ ] Update documentation about type exports
Optional Enhancements
- [ ] Add
stripInternalto hide internal APIs - [ ] Use
@internalJSDoc tags for internal-only exports - [ ] Generate API documentation from types
- [ ] Set up type-testing framework
Migration Strategy
Step-by-Step Rollout
- Test in One Package First: Start with
@verist/core - Verify Consumer Experience: Test in a separate test project
- Roll Out to All Packages: Apply to entire monorepo
- Update Documentation: Explain type exports in README
Verification Steps
# 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 workRollback Plan
If issues arise:
- Revert package.json exports to
./src/index.ts - Keep generated
.d.tsin dist but don't reference - Debug issue separately without blocking development
Technical Details
Current Build Process
From scripts/build.ts:
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)
// 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:
{
"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
{
"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
{
"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:
- Don't export from
index.ts(already doing this) - Use
@internalJSDoc tag - Enable
stripInternalcompiler 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
| Statement | Confidence |
|---|---|
| Current setup works but isn't optimal | 0.9 |
| Should switch to .d.ts before v1.0 | 0.85 |
| tsc is the best tool for this project | 0.8 |
| No immediate rush to implement | 0.85 |
| Will prevent future issues | 0.9 |
References
- TypeScript: Publishing Declaration Files
- TypeScript: Declaration Maps
- Bun Bundler Docs
- Node.js Package Entry Points
- Bun GitHub: @types/bun package.json
Next Steps
- Review this plan with team/stakeholders
- Choose timing: Now vs. pre-release
- Select tool: tsc (recommended) vs. tsup
- Create implementation ticket if approved
- 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.