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 futureFIXME: A known problem that needs fixingHACK: A temporary workaroundNOTE: An important caveatWARNING: 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:
- Remove self-evident comments
- Remove commented-out code (history is managed by Git)
- Add comments that explain intent and constraints
- 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
What to read next
- Variables — reduce comments with self-documenting variable names
- Function design — convey intent with function names