Best ideas from Clean Code by Robert C. Martin (with Typescript examples)

Turbo Ninh
6 min readFeb 5, 2023

--

Writing clean and maintainable code is an essential aspect of software development. Clean code not only makes it easier to understand and maintain, but also helps reduce the risk of bugs and security vulnerabilities. In this article, we’ll explore some of the best ideas from the book Clean Code by Robert C. Martin for writing clean code, along with Typescript examples.

Function Names

Give your functions descriptive and meaningful names. Function names should describe what the function does, making it easier to understand the code at a glance.

function calculateTotalPrice(items: Item[]): number { ... }
function sendEmailToUser(user: User, message: string): void { ... }

Function Length

Function Length: Keep functions short and concise. A good rule of thumb is to keep them under 20 lines of code.

// bad example
function processData(data: any[]) {
data.sort((a, b) => a.value - b.value);
const filteredData = data.filter(item => item.value > 10);
const result = filteredData.map(item => item.name).join(", ");
return result;
}

// good example
function sortData(data: any[]) {
return data.sort((a, b) => a.value - b.value);
}

function filterData(data: any[]) {
return data.filter(item => item.value > 10);
}

function extractNames(data: any[]) {
return data.map(item => item.name).join(", ");
}

function processData(data: any[]) {
const sortedData = sortData(data);
const filteredData = filterData(sortedData);
const result = extractNames(filteredData);
return result;
}

Single Responsibility Principle

Each function should have a single, well-defined responsibility. This makes it easier to maintain and update your code, as well as to reuse functions in other parts of your code.

// bad example
class User {
private name: string;
private email: string;
private password: string;

constructor(name: string, email: string, password: string) {
this.name = name;
this.email = email;
this.password = password;
}

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

public getEmail(): string {
return this.email;
}

public hashPassword(): string {
// hash password logic
return this.password;
}
}

// good example
class User {
private name: string;
private email: string;
private password: string;

constructor(name: string, email: string, password: string) {
this.name = name;
this.email = email;
this.password = password;
}

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

public getEmail(): string {
return this.email;
}
}

class PasswordHasher {
public static hashPassword(password: string): string {
// hash password logic
return password;
}
}

Readability

Make your code easy to read by using clear and concise variable names, adding comments where necessary, and breaking up complex functions into smaller, more manageable pieces.

// Clear and concise variable names
const userName = getUserName(userId);
const formattedDate = formatDate(date);

// Comments to explain complex logic
function calculateDiscount(price: number, quantity: number): number {
// Apply 10% discount for orders over 100 items
if (quantity >= 100) {
return price * 0.9;
}

// No discount for orders under 100 items
return price;
}

// Breaking up complex functions into smaller pieces
function applyDiscount(price: number, discount: number): number {
return price * (1 - discount);
}

function calculateDiscount(price: number, quantity: number): number {
if (quantity >= 100) {
return applyDiscount(price, 0.1);
}

return price;
}

Avoid Magic Numbers

Avoid using “magic numbers,” or hardcoded values that have no clear meaning, in your code. Instead, use descriptive constants or enumerations to make the code more readable and maintainable. For example:

// Avoid magic numbers
function calculateDiscount(price: number, quantity: number): number {
if (quantity >= 100) {
return price * 0.9;
}

if (quantity >= 50) {
return price * 0.95;
}

return price;
}

// Use descriptive constants
const MIN_QUANTITY_FOR_10_PERCENT_DISCOUNT = 100;
const MIN_QUANTITY_FOR_5_PERCENT_DISCOUNT = 50;

function calculateDiscount(price: number, quantity: number): number {
if (quantity >= MIN_QUANTITY_FOR_10_PERCENT_DISCOUNT) {
return price * 0.9;
}

if (quantity >= MIN_QUANTITY_FOR_5_PERCENT_DISCOUNT) {
return price * 0.95;
}

return price;
}

DRY Principle

Don’t Repeat Yourself. Reuse code as much as possible, and refactor duplicate code into reusable functions and modules.

// bad example
function validateEmail(email: string): boolean {
const regex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
return regex.test(email);
}

function validatePassword(password: string): boolean {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return regex.test(password);
}

// good example
const emailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;

function validate(value: string, regex: RegExp): boolean {
return regex.test(value);
}

function validateEmail(email: string): boolean {
return validate(email, emailRegex);
}

function validatePassword(password: string): boolean {
return validate(password, passwordRegex);
}

Error Handling

Handle errors gracefully by using clear and descriptive error messages, logging errors where necessary, and using appropriate error handling techniques to prevent the code from crashing. For example:

// Use try-catch for synchronous errors
try {
const user = getUser(userId);
console.log(`User: ${user.name}`);
} catch (error) {
console.error(`Error: ${error.message}`);
}

// Use promises and async-await for asynchronous errors
async function getOrders(userId: number): Promise<Order[]> {
try {
const orders = await fetchOrders(userId);
return orders;
} catch (error) {
console.error(`Error: ${error.message}`);
throw error;
}
}

Additionally, it’s also a good practice to use exceptions only for exceptional situations. Use return codes or error objects instead of exceptions when the error is expected to happen often. For example:

// Use exceptions for exceptional situations
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}

// Use return codes or error objects for expected errors
interface Result {
result: number;
error: string | null;
}

function divideWithReturnCode(a: number, b: number): Result {
if (b === 0) {
return { result: 0, error: "Cannot divide by zero" };
}
return { result: a / b, error: null };
}

This approach helps to improve the performance and readability of the code, as exceptions have a performance cost and can be harder to trace than return codes or error objects. Additionally, it also makes it easier to understand the purpose of the code, as the error handling logic is clearly separated from the main logic.

Simple Code

Keep the code simple and straightforward. Avoid using complex algorithms, convoluted control flow structures, or unnecessary abstractions. Below is an example of a bad implementation:

// Bad Example: Complex for loop to sum an array of numbers
function sumArray(numbers: number[]): number {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
for (let j = 0; j < numbers[i]; j++) {
sum++;
}
}
return sum;
}

And a better implementation:

// Simple for loop to sum an array of numbers
function sumArray(numbers: number[]): number {
let sum = 0;
for (const number of numbers) {
sum += number;
}
return sum;
}

Avoid Global Variables

Avoid using global variables, as they can make your code difficult to maintain and debug. Instead, use modules and functions to encapsulate state and behavior.

// bad example
let name = "John Doe";

function getName() {
return name;
}

function setName(newName: string) {
name = newName;
}

// good example
function createNameModule() {
let name = "John Doe";

return {
getName: () => name,
setName: (newName: string) => { name = newName; }
}
}

const nameModule = createNameModule();
const currentName = nameModule.getName();
nameModule.setName("Jane Doe");

Test-driven Development

Write tests first, and then write the code to make the tests pass. This helps ensure that your code works as intended, and makes it easier to maintain and update.

// good example
function add(a: number, b: number): number {
return a + b;
}

describe("add", () => {
it("should add two numbers", () => {
const result = add(1, 2);
expect(result).toBe(3);
});
});

Keep the Most Important Code at the Top

Put the most important code at the top of the file, followed by less important code, so that readers can quickly find and understand the core parts of the code. This makes it easier for others to understand and maintain your code.

// createAnimal.ts

// Good Example: Most important code at the top
function createAnimal(name: string, food: string, hours: number) {
console.log(`Creating animal ${name}...`);
eat(food);
sleep(hours);
console.log(`${name} has been created.`);
}

// Other functions and code
function eat(food: string) {
console.log(`Eating ${food}...`);
}

function sleep(hours: number) {
console.log(`Sleeping for ${hours} hours...`);
}

Code Organization

Organize your code into distinct sections, making it easier to read and maintain. Group similar functions and classes together, and separate them with clear and meaningful comments.

// Utility functions section
function getUserName(userId: number): string { ... }
function formatDate(date: Date): string { ... }

// Database access section
class UserRepository { ... }
class OrderRepository { ... }

Code Formatting

Adhere to a consistent code formatting style, such as using spaces and indentation correctly. Formatting code correctly makes it easier to read, understand, and maintain. A good code formatting style should be consistent, clear, and concise. For example:

// Good Example: Code Formatting
function calculateSum(numbers: number[]): number {
let sum = 0;
for (const number of numbers) {
sum += number;
}
return sum;
}

// Bad Example: Code Formatting
function calculateSum(numbers:number[])
{let sum=0
for(const number of numbers)
{sum+=number}
return sum}

These are just a few examples of the best practices for writing clean code with TypeScript. By following these practices, you can write code that is easy to understand, maintain, and scale.

Also, it’s important to note that these practices are not specific to TypeScript, they are generally recommended for writing clean and maintainable code in any programming language.

--

--

Turbo Ninh

I'm Turbo, a random full-stack Javascript developer based in Copenhagen