JSDoc TypeScript Documentation
Expert-level guidance for TypeScript documentation strategy and decision-making.
Documentation Decision Tree
| Code Type |
Public Library |
Internal Library |
Personal/Small Team |
| Public function |
Always: Full docs + examples + errors |
Always: Brief + non-obvious behavior |
If complex: Why + gotchas only |
| Public interface |
Always: With usage example |
Always: Brief description |
If non-obvious: Purpose only |
| Type alias |
Always: Purpose + example |
If non-obvious: When to use |
Skip: If name + types are clear |
| Private function |
If complex: Algorithm explanation |
If complex: Why it exists |
Skip: Unless tricky |
| Generic param |
Always: Constraint rationale |
Always: What it represents |
If constrained: Why constraint |
| Constants |
If non-obvious: Why this value |
If non-obvious: Why this value |
Skip: Self-explanatory values |
| Error throws |
Always: All possible errors |
Always: Error codes/types |
If non-obvious: What triggers |
Before Documenting, Ask
Audience: Who will read this?
- Library users (npm package) → Full docs: examples, edge cases, performance, migration guides
- Internal team (company-wide) → Focus on "why" decisions, gotchas, design context
- Future you (personal project) → Context for non-obvious choices, workarounds, TODOs
Value: Does the type system already say it?
- Types say WHAT → Document WHY and WHEN (behavior, constraints, use cases)
- Types unclear → Document WHAT first, then WHY
- Types complete → Skip redundant descriptions, document non-obvious behavior only
Maintenance: Will this stay synchronized?
- Stable contracts (public API) → Document thoroughly, changes require migration guides
- Implementation details → Don't document (will diverge), use code comments instead
- Behavior that changes → Document contract in JSDoc, validate in tests
Critical Anti-Patterns
NEVER: Document What Types Already Express
// NEVER: Completely redundant
/**
* Gets the user's name.
* @param user - The user object
* @returns The user's name as a string
*/
function getName(user: User): string {
return user.name;
}
Why: Wastes time, adds clutter, becomes outdated. Types already express this contract.
Instead: Only document if there's non-obvious behavior:
/**
* Gets the user's display name.
* Falls back to email username if name is not set.
* Returns "Anonymous" for guest users.
*/
function getDisplayName(user: User): string {
return user.name || user.email.split('@')[0] || 'Anonymous';
}
NEVER: Skip Error Documentation in Public APIs
// NEVER: Undocumented errors in public function
export async function fetchUser(id: string): Promise<User> {
// Throws NotFoundError, PermissionError, NetworkError - but not documented!
}
Why: Consumers can't handle errors they don't know about. Leads to unhandled exceptions in production.
Instead: Document ALL possible errors:
/**
* Fetches user by ID.
*
* @throws {NotFoundError} User doesn't exist (404)
* @throws {PermissionError} Insufficient permissions (403)
* @throws {NetworkError} Request failed (500)
*/
export async function fetchUser(id: string): Promise<User> {
// Implementation
}
NEVER: Use Template/Placeholder Documentation
// NEVER: Generic template docs
/**
* TODO: Add description
* @param data - The data
* @returns The result
*/
export function processData(data: any): any {
// Implementation
}
Why: Worse than no docs—suggests API is documented when it isn't. Misleads users.
Instead: Either document properly or omit JSDoc entirely:
// Better: No docs than bad docs (though still not ideal for public API)
export function processData(data: ProcessInput): ProcessResult {
// Implementation
}
NEVER: Document Internal Implementation in Public API
// NEVER: Implementation details in public docs
/**
* Fetches users from Redis cache (primary) or PostgreSQL (fallback).
* Uses connection pool with 10 max connections.
* Cache TTL is 5 minutes.
*/
export async function getUsers(): Promise<User[]> {
// Implementation
}
Why: Locks you into implementation. Users depend on Redis, you can't switch to different cache.
Instead: Document observable behavior only:
/**
* Fetches all users.
*
* @remarks
* Results may be cached for up to 5 minutes.
* For real-time data, use {@link getUsersRealtime}.
*/
export async function getUsers(): Promise<User[]> {
// Implementation
}
NEVER: Skip Migration Guides for Breaking Changes
// NEVER: Breaking change without migration guidance
/**
* Creates a user.
*
* @since 2.0.0
* @deprecated The API changed in v2.0
*/
export function createUser(data: NewUserData): Promise<User> {
// What changed? How do I migrate? No guidance!
}
Why: Users can't upgrade without knowing how to migrate.
Instead: Provide explicit migration path:
/**
* Creates a user.
*
* @remarks
* **Breaking Change in v2.0.0**: `name` parameter moved from top-level
* to nested under `metadata.name`.
*
* Migration:
* ```typescript
* // Before (v1.x)
* createUser({ email, password, name });
*
* // After (v2.x)
* createUser({ email, password, metadata: { name } });
* ```
*
* @since 2.0.0
*/
export function createUser(data: NewUserData): Promise<User> {
// Implementation
}
NEVER: Use Non-Executable Examples
// NEVER: Pseudocode that doesn't run
/**
* @example
* ```typescript
* // Call the function with parameters
* myFunction(param1, param2);
* // Process the result
* doSomething(result);
* ```
*/
export function myFunction(a: string, b: number): Result {
// Implementation
}
Why: Examples that don't run mislead users and break when API changes.
Instead: Use real, executable code:
/**
* @example
* ```typescript
* const result = myFunction('hello', 42);
* console.log(result.value); // Output: "hello42"
*
* // With error handling
* try {
* const result = myFunction('', -1);
* } catch (error) {
* console.error('Invalid input:', error.message);
* }
* ```
*/
export function myFunction(a: string, b: number): Result {
// Implementation
}
When to Load References
Load jsdoc-syntax.md when:
- Need specific JSDoc tag syntax (@param, @returns, @throws, etc.)
- Documenting overloaded functions or complex generics
- Need syntax for React components or classes
- Want comprehensive tag reference
Load library-api-docs.md when:
- MANDATORY: Documenting public library for external consumers
- MANDATORY: Writing documentation for npm package
- Need examples of audience-specific documentation depth
- Documenting breaking changes or deprecations
- Need performance documentation patterns
- Creating migration guides
Load typedoc-setup.md when:
- MANDATORY: Setting up automated documentation generation
- MANDATORY: Configuring TypeDoc for the first time
- Integrating documentation into CI/CD pipeline
- Configuring multi-package monorepo documentation
- Setting up GitHub Pages deployment
- Need documentation validation in pre-commit hooks
Quick Wins: High-Impact Documentation
Focus documentation effort on these high-value targets:
- Public API functions: 80% of user questions come from public functions
- Error conditions: Undocumented errors = production incidents
- Complex types: Generic types and conditional types need explanation
- Migration guides: Breaking changes without migration = angry users
- Performance characteristics: O(n²) operations need warnings
- Examples: One good example > 100 lines of prose
Documentation Smells (Warning Signs)
| Smell |
What It Means |
Fix |
| Every function has identical JSDoc |
Template docs, not real docs |
Remove templates, document non-obvious only |
| No @throws tags |
Errors not documented |
Add all possible exceptions |
| No examples in library |
Theory without practice |
Add real, executable examples |
| Docs contradict types |
Out of sync |
Update docs or simplify (let types speak) |
| Private functions have more docs than public |
Inverted priorities |
Focus on public API first |
TypeDoc Validation
Enforce documentation in CI:
// package.json
{
"scripts": {
"docs:validate": "typedoc --validation.notDocumented true"
}
}
Pre-commit hook:
npm run docs:validate || exit 1
Resources