Writing Self-Documenting Code πŸ§‘β€πŸ’»πŸ“œ

Matt Lewandowski - May 29 - - Dev Community

As developers, we've all wasted hours, staring at a piece of code, trying to decipher its purpose, and wondering what the original author was thinking. In the world of software development, where projects change hands and codebases evolve quickly, writing self-documenting code is not just a nice-to-have; it's a necessity.

In this article, we'll explore the art of crafting code that speaks for itself, reducing the reliance on external documentation and making life easier for yourself and your fellow developers.

What is Self-Documenting Code?

Self-documenting code is code that is written in a clear, expressive, and intentional manner, making its purpose and functionality easily understandable without the need for extensive comments or external documentation.

It's about writing code that is:

  • Readable: Code that is easy to read and understand at a glance
  • Expressive: Code that clearly conveys its intent and purpose
  • Maintainable: Code that is easy to modify and update without introducing bugs or breaking changes

Why Self-Documenting Code Matters

Writing self-documenting code offers several benefits:

  1. Reduced Cognitive Load: When code is self-explanatory, developers can quickly grasp its purpose and functionality, reducing the mental effort required to understand and work with the codebase.

  2. Faster Onboarding: New team members can get up to speed more quickly when the codebase is self-documenting, as they don't need to rely heavily on external documentation or extensive knowledge transfer sessions.

  3. Improved Collaboration: Self-documenting code facilitates better collaboration among team members, as it minimizes misunderstandings and promotes a shared understanding of the codebase.

  4. Enhanced Maintainability: When code is self-documenting, it's easier to maintain and update over time, as developers can quickly understand the existing code and make informed changes.

Techniques for Writing Self-Documenting Code

Let's explore some practical techniques for writing self-documenting code, with a focus on TypeScript:

1. Use Meaningful Names

One of the most effective ways to make your code self-documenting is to use meaningful names for variables, functions, classes, and modules. Consider the following example:

// Bad
const x = 5;
const y = 10;
const z = x + y;

// Good
const numberOfItems = 5;
const itemPrice = 10;
const totalCost = numberOfItems * itemPrice;
Enter fullscreen mode Exit fullscreen mode

In the "Good" example, the variable names clearly convey their purpose, making the code more readable and self-explanatory.

2. Write Small, Focused Functions

Writing small, focused functions is another key aspect of self-documenting code. Functions should have a single responsibility and be named accurately to reflect their purpose. For example:

// Bad
function processData(data: any): any {
    // ...
    // Lots of complex logic
    // ...
    return result;
}

// Good
function extractRelevantFields(data: Record<string, any>): Record<string, any> {
    // ...
    return relevantFields;
}

function applyBusinessRules(relevantFields: Record<string, any>): Record<string, any> {
    // ...
    return processedData;
}

function formatOutput(processedData: Record<string, any>): string {
    // ...
    return formattedResult;
}
Enter fullscreen mode Exit fullscreen mode

By breaking down a large function into smaller, focused functions with descriptive names, the code becomes more readable and self-documenting.

3. Use Descriptive Function and Method Names

When naming functions and methods, use descriptive names that clearly convey their purpose and action. Avoid generic names like handle() or process(). Instead, opt for names that describe what the function does. For example:

// Bad
function handleInput(input: string): void {
    // ...
}

// Good
function validateUserCredentials(username: string, password: string): boolean {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The descriptive name validateUserCredentials makes it clear what the function does without the need for additional comments.

4. Leverage TypeScript's Type System

TypeScript's powerful type system can greatly enhance the self-documenting nature of your code. By leveraging TypeScript's features, you can make your code more expressive and catch potential errors early. For example:

  • Interfaces and Types: Use interfaces and custom types to define the shape of your data structures, making the code more readable and self-explanatory.
interface User {
    id: number;
    name: string;
    email: string;
}

function getUserById(id: number): User | undefined {
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • Enums: Utilize enums to represent a fixed set of values, providing a clear and readable way to handle different scenarios.
enum PaymentStatus {
    Pending = 'pending',
    Completed = 'completed',
    Failed = 'failed',
}

function processPayment(status: PaymentStatus): void {
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • Type Inference: Let TypeScript infer types whenever possible, as it reduces verbosity and makes the code more readable.
// Bad
const count: number = 10;
const message: string = 'Hello, world!';

// Good
const count = 10;
const message = 'Hello, world!';
Enter fullscreen mode Exit fullscreen mode

5. Use Strongly Typed IDs

When working with IDs in TypeScript, consider using strongly typed IDs instead of simple strings or numbers. Strongly typed IDs provide additional type safety and make the code more self-documenting.

One way to implement strongly typed IDs is by using opaque types:

// user.ts
export type UserId = string & { readonly __brand: unique symbol };

export function createUserId(id: string): UserId {
    return id as UserId;
}

// post.ts
export type PostId = string & { readonly __brand: unique symbol };

export function createPostId(id: string): PostId {
    return id as PostId;
}
Enter fullscreen mode Exit fullscreen mode

By using strongly typed IDs, you can ensure that a UserId can only be assigned to properties or functions expecting a UserId, and a PostId can only be assigned to properties or functions expecting a PostId.

function getUserById(userId: UserId): User | undefined {
    // ...
}

const userId = createUserId('user-123');
const postId = createPostId('post-456');

getUserById(userId); // No error
getUserById(postId); // Error: Argument of type 'PostId' is not assignable to parameter of type 'UserId'.
Enter fullscreen mode Exit fullscreen mode

Strongly typed IDs help catch potential errors at compile-time and make the code more expressive and self-documenting. However, they do introduce some overhead compared to using simple string types. Consider the trade-offs based on your project's needs and scale.

6. Establish Consistency and Set Expectations in a Team

When working on a codebase as part of a team, establishing consistency and setting clear expectations is crucial. This helps ensure that everyone is on the same page and follows the same conventions, making the code more readable and maintainable.

One important aspect of consistency is naming conventions. Establish a style guide that defines how variables, functions, classes, and other entities should be named. For example, consider the following terms and their meanings:

  • get: Retrieves a single value from an API or data source.
  • list: Retrieves a set of values from an API or data source.
  • patch: Partially updates an existing entity or object.
  • upsert: Updates an existing entity or inserts a new one if it - doesn't exist.

By defining these terms and their usage, you can ensure consistency across the codebase. For example:

function getUser(userId: UserId): Promise<User> {
    // ...
}

function listUsers(): Promise<User[]> {
    // ...
}

function patchUser(userId: UserId, updatedData: Partial<User>): Promise<User> {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Consistency helps make the codebase more predictable and easier to understand for all team members. In addition to naming conventions, consider establishing guidelines for other aspects of the codebase, such as file and folder structure, comments, error handling, testing, and code formatting.

When working in a team, you might not always personally agree with all of the conventions that are being enforced. However, it's important to remember that consistency and collaboration are crucial for the success of the project. Even if you have a different preference or coding style, adhering to the agreed conventions helps maintain a cohesive codebase and reduces confusion among other team members.

7. Use JSDoc or TSDoc for Complex Scenarios

While self-documenting code should be the primary goal, there may be cases where additional documentation is necessary, such as complex algorithms or intricate business logic. In such scenarios, consider using JSDoc or TSDoc to provide clear and concise documentation.

/**
 * Calculates the Fibonacci number at the given position.
 *
 * @param {number} position - The position of the Fibonacci number to calculate.
 * @returns {number} The Fibonacci number at the specified position.
 */
function fibonacci(position: number): number {
    if (position <= 1) {
        return position;
    }
    return fibonacci(position - 1) + fibonacci(position - 2);
}
Enter fullscreen mode Exit fullscreen mode

By using JSDoc or TSDoc, you can provide additional context and explanations for complex code without cluttering the codebase itself.

Conclusion

Writing self-documenting code is an art that every developer should strive to master. By leveraging meaningful names, small and focused functions, TypeScript's type system, and judicious use of documentation, you can create code that is readable, expressive, and maintainable.

Remember, the goal is to write code that speaks for itself, reducing the reliance on external documentation and making the lives of your fellow developers easier. So, the next time you're writing code, take a moment to consider how you can make it more self-documenting. Your future self and your teammates will thank you!

Also, shameless plug πŸ”Œ. If you work in an agile dev team and use tools for your online meetings like planning poker or retrospectives, check out my free tool called Kollabe!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .