Skip to main content

Comments

Why are comments necessary

Ideally, good code can be understood without comments. However, comments are useful in the following cases:

When comments are needed
  • Explaining the Why
  • Explaining the intent of a complex algorithm
  • Noting caveats and constraints
  • Markers such as TODO and FIXME
  • Documentation for public APIs (JSDoc)

The downside of comments

Problems:

  • Comments are not checked by the compiler
  • When code changes, comments are sometimes not updated
  • Stale comments cause misunderstanding

Conclusion: Prefer self-documenting code over comments.


Express intent with code

❌ Bad: Explaining with a comment

❌ Bad: Explaining with a comment
// Returns true if the user is an admin or editor
if (user.role === 'admin' || user.role === 'editor') {
// Allow editing
allowEdit = true;
}

✅ Good: Express intent with code

✅ Good: Express intent with code
// Express intent with the function name
function canEdit(user: User): boolean {
return user.role === 'admin' || user.role === 'editor';
}

if (canEdit(user)) {
allowEdit = true;
}

❌ Bad: Relying on a comment

❌ Bad: Relying on a comment
// Calculate the consumption tax from the price and return the total amount
function calc(p: number): number {
return p * 1.1;
}

✅ Good: A self-documenting function

✅ Good: A self-documenting function
function calculatePriceWithTax(price: number, taxRate: number = 0.1): number {
return price * (1 + taxRate);
}

Don't state the obvious

❌ Bad: Self-evident comments

❌ Bad: Self-evident comments
// Get the user name
function getUserName(user: User): string {
return user.name;
}

// Increment the counter
counter++;

// Increase i by 1
i++;

// User class
class User {
// Name
name: string;

// Email address
email: string;
}

These comments are unnecessary because you can tell just by reading the code.

✅ Good: Clear without comments

✅ Good: Clear without comments
function getUserName(user: User): string {
return user.name;
}

counter++;
i++;

class User {
name: string;
email: string;
}

Good comments

1. Comments that explain the Why

✅ Good: Explain why you wrote this implementation
// ✅ Good: Explain why you wrote this implementation

function getUsers(): User[] {
// Cache the user list to improve performance
// The API allows only one call per minute
if (cache.has('users') && !cache.isExpired('users', 60000)) {
return cache.get('users');
}

const users = fetchUsersFromApi();
cache.set('users', users);
return users;
}

// ✅ Good: Constraints and caveats
function processPayment(amount: number): void {
// Caution: this process is not idempotent
// Running the same request multiple times causes duplicate charges
chargeCustomer(amount);
}

2. Explaining a complex algorithm

✅ Good: Explain the algorithm's intent
// ✅ Good: Explain the algorithm's intent

/**
* Validate a credit card number using the Luhn algorithm
* https://en.wikipedia.org/wiki/Luhn_algorithm
*/
function validateCreditCard(cardNumber: string): boolean {
const digits = cardNumber.replace(/\D/g, '').split('').map(Number);

// Starting from the second digit from the right, double every other digit
for (let i = digits.length - 2; i >= 0; i -= 2) {
digits[i] *= 2;
// If it becomes two digits, add the digits together (e.g. 12 → 1 + 2 = 3)
if (digits[i] > 9) {
digits[i] -= 9;
}
}

const sum = digits.reduce((acc, digit) => acc + digit, 0);
return sum % 10 === 0;
}

3. TODO and FIXME markers

// TODO: Implement pagination
function getUsers(): User[] {
return allUsers;
}

// FIXME: This process is O(n^2), so performance is poor
function findDuplicates(items: string[]): string[] {
const duplicates: string[] = [];
for (let i = 0; i < items.length; i++) {
for (let j = i + 1; j < items.length; j++) {
if (items[i] === items[j]) {
duplicates.push(items[i]);
}
}
}
return duplicates;
}

// HACK: A temporary workaround (the API should really be fixed)
const data = apiResponse.data?.result?.[0] ?? {};

// NOTE: This constant depends on an external API's spec
const MAX_ITEMS_PER_PAGE = 50;

Common markers:

  • TODO: Something to implement in the future
  • FIXME: A known problem that needs fixing
  • HACK: A temporary workaround
  • NOTE: An important caveat
  • WARNING: A warning

4. Documentation for public APIs (JSDoc)

For APIs you expose in TypeScript, use JSDoc comments.

/**
* Find a user by ID
*
* @param userId - The unique ID of the user
* @returns The user object, or undefined if not found
*
* @example
* ```typescript
* const user = getUserById("12345");
* if (user) {
* console.log(user.name);
* }
* ```
*/
function getUserById(userId: string): User | undefined {
return users.find(user => user.id === userId);
}

/**
* Create a user
*
* @param name - The user name
* @param email - The email address (must be unique)
* @returns The created user object
* @throws {ValidationError} If the email address is invalid
* @throws {DuplicateError} If the email address is already in use
*/
function createUser(name: string, email: string): User {
if (!isValidEmail(email)) {
throw new ValidationError('Invalid email address');
}

if (emailExists(email)) {
throw new DuplicateError('Email already exists');
}

return new User(generateId(), name, email);
}

/**
* Search users with multiple criteria
*
* @param options - Search options
* @param options.role - The user role (optional)
* @param options.isActive - The active state (optional)
* @param options.limit - The maximum number of results (default: 100)
* @returns An array of users matching the criteria
*/
function searchUsers(options: {
role?: UserRole;
isActive?: boolean;
limit?: number;
}): User[] {
// ...
}

5. Explaining a non-intuitive implementation

✅ Good: Explain why this order is used
// ✅ Good: Explain why this order is used

function initializeApp(): void {
loadConfig();
connectDatabase();
startServer();

// The cache must be initialized after the server starts
// because initializing the cache requires an HTTP request
initializeCache();
}

// ✅ Good: Explaining a special case

function parseDate(dateString: string): Date {
// Safari treats "YYYY-MM-DD HH:mm:ss" (space-separated) as an Invalid Date,
// so convert it to the ISO 8601 format "YYYY-MM-DDTHH:mm:ss" (T-separated), which is required by spec
const isoDateString = dateString.replace(' ', 'T');
return new Date(isoDateString);
}

Bad comments

1. Repeating the code

❌ Bad: Repeating the code verbatim
// ❌ Bad: Repeating the code verbatim
// Assign user.name to userName
const userName = user.name;

// ❌ Bad
// Loop while i is less than 10
for (let i = 0; i < 10; i++) {
// ...
}

// ❌ Bad
// User class
class User {
// name property
name: string;
}

2. Misleading comments

❌ Bad: The comment does not match the code
// ❌ Bad: The comment does not match the code

// Get all users
function getActiveUsers(): User[] {
return users.filter(user => user.isActive); // Only active users
}

// ❌ Bad: A stale comment
// Retry up to 3 times
const MAX_RETRIES = 5; // The comment contradicts the value

3. Commented-out code

❌ Bad: Leaving commented-out code
// ❌ Bad: Leaving commented-out code

function calculateTotal(items: Item[]): number {
// const tax = 0.08;
// const subtotal = items.reduce((sum, item) => sum + item.price, 0);
// return subtotal * (1 + tax);

return items.reduce((sum, item) => sum + item.price, 0);
}

Reasons:

  • Because you have a version control system (Git), old code can be restored from history
  • Commented-out code causes confusion

4. Excessive comments

❌ Bad: Excessive
// ❌ Bad: Excessive

// A class that holds user information
class User {
// User ID
private id: string;

// User name
private name: string;

// Email address
private email: string;

// Constructor
// @param id User ID
// @param name User name
// @param email Email address
constructor(id: string, name: string, email: string) {
// Assign id to this.id
this.id = id;
// Assign name to this.name
this.name = name;
// Assign email to this.email
this.email = email;
}

// A method that gets the user name
// @returns User name
getName(): string {
// Return this.name
return this.name;
}
}

// ✅ Good: Simple and clear
class User {
constructor(
private id: string,
private name: string,
private email: string
) {}

getName(): string {
return this.name;
}
}

Best practices for comments in TypeScript

1. Don't write type information in comments

Because TypeScript has a type system, you don't need to write type information in comments.

❌ Bad: Explaining types with comments
// ❌ Bad: Explaining types with comments
// @param userId User ID (string)
// @returns User object (User or undefined)
function getUserById(userId) {
// ...
}

// ✅ Good: Express it with the type system
function getUserById(userId: string): User | undefined {
// ...
}

// ✅ When more detailed documentation is needed
/**
* Find a user by ID
*
* @param userId - The unique identifier of the user
* @returns The user found, or undefined if it does not exist
*/
function getUserById(userId: string): User | undefined {
// ...
}

2. Add descriptions to interfaces

✅ Good: Explain the interface's intent
// ✅ Good: Explain the interface's intent

/**
* An interface representing user information
*/
interface User {
/** The unique ID of the user */
id: string;

/** Display name */
name: string;

/** Email address (unique) */
email: string;

/** Account creation timestamp */
createdAt: Date;

/**
* User role
* @default 'user'
*/
role?: UserRole;
}

3. Add comments to complex types

✅ Good: Explain a complex type
// ✅ Good: Explain a complex type

/**
* The API response type
* On success it includes data; on failure it includes error
*/
type ApiResponse<T> =
| { success: true; data: T; error?: never }
| { success: false; data?: never; error: string };

/**
* The type of a user permission-check function
* @param user - The user to check
* @param resource - The resource to access
* @returns If true, access is allowed
*/
type PermissionChecker = (user: User, resource: string) => boolean;

Exercise 6: Improving comments

Improve the comments in the following code.

❌ Bad: Before
// ❌ Before

// User class
class User {
// Name
name: string;

// Age
age: number;

// Constructor
// Takes a name and an age
constructor(name: string, age: number) {
// Assign name to this.name
this.name = name;
// Assign age to this.age
this.age = age;
}

// Determine whether the user is an adult
// True if 20 or older
isAdult(): boolean {
// Return true if age is 20 or older
return this.age >= 20;
}
}

// A function that gets a user
// Takes an id and returns a user
function getUser(id: string): User {
// Search the user list by id
// const user = users.find(u => u.id === id); // Old code

// Get it from the cache
return cache.get(id);
}
📝 View answer
✅ Good: After
// ✅ After

/**
* A class representing user information
*/
class User {
constructor(
public name: string,
public age: number
) {}

/**
* Determine whether the user is an adult (20 or older)
*/
isAdult(): boolean {
const ADULT_AGE = 20;
return this.age >= ADULT_AGE;
}
}

/**
* Get a user by ID
*
* To improve performance, it reads from the cache rather than the database.
* The caller must handle the case where the cache misses.
*
* @param id - User ID
* @returns The cached user object, or undefined if it does not exist
*/
function getUser(id: string): User | undefined {
return cache.get(id);
}

Improvements:

  1. Remove self-evident comments
  2. Remove commented-out code (history is managed by Git)
  3. Add comments that explain intent and constraints
  4. Document the public API with JSDoc

Summary of comments

Comment guidelines
  • Don't comment what the code can express
  • Write the Why, not the What
  • Add explanations to complex algorithms
  • Use JSDoc for public APIs
  • Use markers such as TODO / FIXME
  • Remove commented-out code
  • Express type information with the type system, not comments

Principle: Prefer self-documenting code over comments


  • Variables — reduce comments with self-documenting variable names
  • Function design — convey intent with function names