Lesson 18 of 54

Comments and Self-Documenting Code

Documentation That Matters

Code comments are one form of documentation. But there's another form that matters deeply: API documentation.

When you write code that others will use—libraries, public APIs, shared modules—formal documentation becomes essential. This isn't about explaining implementation details. It's about explaining the contract.

When to Write Formal Documentation

Write Documentation For:

Public APIs and Libraries

Typescript
/**
 * Sends an email through the configured mail provider.
 * 
 * @param to - Recipient email address
 * @param subject - Email subject line (max 998 characters per RFC 2822)
 * @param body - Email body (HTML supported)
 * @returns Promise resolving to the message ID
 * @throws {InvalidEmailError} If recipient address is malformed
 * @throws {RateLimitError} If sending rate exceeds 100/minute
 * 
 * @example
 * const messageId = await sendEmail(
 *   'user@example.com',
 *   'Welcome!',
 *   '<h1>Thanks for signing up</h1>'
 * );
 */
async function sendEmail(to: string, subject: string, body: string): Promise<string>

Complex Algorithms

Javascript
/**
 * Implements Levenshtein distance calculation using Wagner-Fischer algorithm.
 * 
 * Time complexity: O(m*n) where m and n are string lengths
 * Space complexity: O(min(m,n)) using single-row optimization
 * 
 * @see https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
 */
function levenshteinDistance(a: string, b: string): number

Configuration and Setup

Javascript
/**
 * Database connection configuration.
 * 
 * @property host - Database server hostname (default: 'localhost')
 * @property port - Database port (default: 5432)
 * @property maxConnections - Maximum pool size. Set based on 
 *   (core_count * 2) + effective_spindle_count for optimal performance.
 * @property idleTimeout - How long idle connections stay open (ms)
 */
interface DatabaseConfig { }

Skip Documentation For:

Internal implementation details — These change frequently and documentation quickly goes stale.

Obvious codegetName() doesn't need a docstring saying "Gets the name."

Private methods — Unless they're complex, the code should speak for itself.

The Elements of Good API Documentation

1. Description

What does this do at a high level?

Typescript
/**
 * Validates and normalizes a phone number to E.164 format.
 * Handles various input formats including local, national, and international.
 */
function normalizePhoneNumber(input: string, countryCode?: string): string

2. Parameters

What does each parameter accept?

Typescript
/**
 * @param input - Raw phone number string in any common format
 * @param countryCode - ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB'). 
 *   Required if input doesn't include country code.
 */

Document:

  • What format is expected
  • What values are valid
  • What happens with edge cases

3. Return Value

What comes back?

Typescript
/**
 * @returns Phone number in E.164 format (e.g., '+14155551234')
 *   or null if the number cannot be parsed
 */

4. Errors and Exceptions

What can go wrong?

Typescript
/**
 * @throws {InvalidPhoneError} If the number is malformed
 * @throws {UnsupportedCountryError} If the country is not supported
 */

5. Examples

Show it in action:

Typescript
/**
 * @example
 * // US number with country code
 * normalizePhoneNumber('+1 (415) 555-1234')  // Returns: '+14155551234'
 * 
 * @example
 * // UK number without country code
 * normalizePhoneNumber('020 7946 0958', 'GB')  // Returns: '+442079460958'
 * 
 * @example
 * // Invalid number
 * normalizePhoneNumber('not-a-number')  // Throws: InvalidPhoneError
 */

Examples are often the most valuable part of documentation.

JSDoc/TSDoc Best Practices

Do Document

Typescript
/**
 * Creates a new user account.
 * 
 * @param userData - User information for account creation
 * @param options - Additional options for account setup
 * @returns The created user with generated ID and timestamps
 * @throws {ValidationError} If required fields are missing
 * @throws {DuplicateEmailError} If email is already registered
 */
async function createUser(
  userData: CreateUserInput,
  options?: CreateUserOptions
): Promise<User>

Don't Duplicate the Obvious

Typescript
// BAD: Adds nothing
/**
 * @param id - The id
 * @returns The user
 */
function getUser(id: string): User

// BETTER: Adds context
/**
 * @param id - User's unique identifier (UUID v4)
 * @returns User object, or throws if not found
 * @throws {NotFoundError} If no user exists with this ID
 */
function getUser(id: string): User

Keeping Documentation in Sync

Documentation that lies is worse than no documentation. Keep it accurate:

1. Document Close to Code

Put documentation next to the code it documents. Separate wiki pages go stale.

2. Include Documentation in Code Review

When reviewing a function change, check if the documentation still matches.

3. Use TypeScript/Type Annotations

Types are documentation that can't lie—the compiler enforces them.

4. Generate from Source

Tools like TypeDoc, JSDoc, and Swagger generate documentation from code comments. This keeps them closer to the source of truth.

5. Test Your Examples

Example code in documentation should be tested:

Typescript
/**
 * @example
 * ```typescript
 * // This code is extracted and run as a test
 * expect(add(2, 3)).toBe(5);
 * ```
 */

README Documentation

For projects and packages, the README is essential:

What to Include

  1. What it does — One paragraph summary
  2. How to install — Package manager commands
  3. Quick start — Minimal example to get running
  4. API overview — Key functions and concepts
  5. Configuration — Environment variables, options
  6. Contributing — How to contribute (if applicable)

What to Avoid

  • Implementation details that will change
  • Extensive API reference (generate that separately)
  • Obvious information ("This is a JavaScript library")

Architecture Documentation

Sometimes you need higher-level documentation:

Architecture Decision Records (ADRs)

Document significant decisions:

Markdown
# ADR 001: Use PostgreSQL for User Data

## Status
Accepted

## Context
We need to store user data with complex relationships.

## Decision
We will use PostgreSQL.

## Consequences
- Need to manage database migrations
- Team needs SQL knowledge
- Good query performance for our access patterns

System Diagrams

For complex systems, diagrams help:

  • Component relationships
  • Data flow
  • Deployment architecture

But keep them high-level. Detailed diagrams go stale instantly.


Key insight: Documentation serves different purposes at different levels. Code comments explain implementation intent. API documentation explains contracts. Architecture documentation explains decisions. Match the documentation type to the need, and always keep it in sync.