Expert guidance for TypeScript/JSDoc documentation decisions...
Expert-level guidance for TypeScript documentation strategy and decision-making.
| 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 |
Audience: Who will read this?
Value: Does the type system already say it?
Maintenance: Will this stay synchronized?
// 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: 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: 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: 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: 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: 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
}
jsdoc-syntax.md when:library-api-docs.md when:typedoc-setup.md when:Focus documentation effort on these high-value targets:
| 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 |
Enforce documentation in CI:
// package.json
{
"scripts": {
"docs:validate": "typedoc --validation.notDocumented true"
}
}
Pre-commit hook:
npm run docs:validate || exit 1