Skip to main content

Functions Rule One to write Clean TypeScript

Writing clean code means writing clean functions or methods. And to do so, I have chosen the ONE rule. It's not one rule, it's the Air Force One of function rules. A rule to rule them all.

The code you write is meant to be read several times, so it is important to pay attention to how you write it. Especially where you put the logic, your functions, and methods. Logic must be easy to grab and change. Maintainable functions are the foundation of maintainable code.

In this article, I will show you some guides to writing simple, clean methods and functions in TypeScript. All of the tips are made to adhere to this classical principle:

Functions should do one thing. They should do it well. They should do it only.

When you're done, you'll be able to spot bad smells on your code and have criteria to clean them up. And to remember these guidelines you only have to remember the ONE.

1️⃣ Input argument

A function must do one thing. Having a lot of arguments may hide having several responsibilities. And by hiding, I mean that it is not clearly defined in its name.

// ❌
function saveTripBooking(
  clientBooking: Booking,
  paymentData: Payment,
  confirmationMessage: Message
) {
  // make payment
  // save booking
  // send a confirmation email
}

// ✅
function makePayment(clientBooking: Booking) {}
function saveBooking(paymentData: Payment) {}
function sendConfirmationEmail(confirmationMessage: Message) {}

Sometimes all the arguments are related to one mission. In such cases, those variables should be encapsulated in a structure, avoiding the primitive obsession code smell.

// ❌
function sendMessage(
  senderName: string,
  senderAddress: string,
  recipientAddress: string,
  subject: string,
  body: string
) {}

// ✅
type Message = {
  senderName: string;
  senderAddress: string;
  recipientAddress: string;
  subject: string;
  body: string;
};
function sendMessage(message: Message) {}

1️⃣ Structural block level

Conditional and repetitive structures are our bread and butter. So keeping them clean is a worthwhile effort. Problems arise when you start nesting one structure in one another like Russian dolls.

You avoid this pyramid of hell with two simple hygienic habits.

  • Extract the inner block content to a new function.
  • Early return from invalid or trivial cases.
// ❌
function sendTripDetails() {
  const passengers: Passenger[] = getPassengers();
  if (passengers.length >= 0) {
    for (i = 0; i <= passengers.length; i++) {
      if (passenger[1].acceptedCommunications) {
        if (passengers[i].emailAddress) {
          sendTripDetailsByEmail(passengers[i].emailAddress);
        }
        if (passengers[i].phoneNumber) {
          sendTripDetailsBySMS(passengers[i].phoneNumber);
        }
      }
    }
  }
}

// ✅
function sendTripDetails() {
  const passengers: Passenger[] = getPassengers();
  if (passengers.length === 0) return;
  for (i = 0; i <= passengers.length; i++) {
    sendTripDetailsToPassenger(passenger);
  }
}
function sendTripDetailsToPassenger(passenger: Passenger) {
  if (passenger.acceptedCommunications == false) return;
  if (passenger.emailAddress) {
    sendTripDetailsByEmail(passengers[i].emailAddress);
  }
  if (passenger.phoneNumber) {
    sendTripDetailsBySMS(passengers[i].phoneNumber);
  }
}

1️⃣ Level of abstraction

Here we come face-to-face with abstraction. You can imagine your code as a big classic hierarchical corporation. Orders go from top to bottom, but each level has its responsibilities (and usually its vocabulary). You know, white collars don`t get their hands dirty.

// ❌
function getAvailablePlaces(tripId: string) {
  const queryTrips = "select capacity from trips where tripId=" + tripId;
  const capacity = db.select(queryTrips);
  const queryBookings =
    "select sum(seats) from bookings where tripId=" + tripId;
  const tripBookedSeats = db.select(queryBookings);
  const free = capacity - tripBookedSeats;
  const OVERBOOKING_FACTOR = 1.05;
  return free * OVERBOOKING_FACTOR;
}

// ✅
// presentation level
function getAvailablePlaces(tripId: string) {
  const freeSeats = getFreeSeats(tripId);
  return calculateAvailable(freeSeats);
}
// logical level
function getFreeSeats() {
  const capacity = selectTripCapacity(tripId);
  const tripBookedSeats = selectTripBookedSeats();
  return capacity - tripBookedSeats;
}
function calculateAvailable(freeSeats: number) {
  const OVERBOOKING_FACTOR = 1.05;
  return freeSeats * OVERBOOKING_FACTOR;
}
// data level
function selectTripCapacity(tripId: string) {
  const queryCapacity = "select capacity from trips where tripId=" + tripId;
  return db.select(queryCapacity);
}
function selectTripBookedSeats(tripId: string) {
  const queryBookings =
    "select sum(seats) from bookings where tripId=" + tripId;
  return db.select(queryBookings);
}

1️⃣ Responsibility; query or command.

Changing the state and consulting the state of any system are two different things. So, a function must choose its role.

// ❌
function saveBooking(booking: Booking): number {
  db.insertBooking(booking); // ❗ mutation
  return db.selectAvailableSeats(booking.tripId); // ❓ question
}

// ✅
function saveBooking(booking: Booking): void {
  db.insertBooking(booking); // ❗ do things
}
function getAvailablePlaces(tripId: string): number {
  return db.selectAvailableSeats(tripId); // ❓ ask things
}

This is even worse when it results in mutating the state of any argument.

// ❌
function discountBooking(booking: Booking): number {
  const discount = booking.price * 0.1;
  booking.price = booking.price - discount; // ❗ mutation
  return discount; // ❓ question like its not mutating
}

// ✅
function discountBooking(booking: Booking): void {
  const discount = calculateDiscount(booking);
  applyDiscount(booking, discount);
}
function calculateDiscount(booking: Booking): number {
  const discount = booking.price * 0.1;
  return discount; ❓
}
function applyDiscount(booking: Booking, discount: number): void {
  booking.price = booking.price - discount; ❗
}

1️⃣ Digit length and ...

Well if you get here, you will see that following these guides, you will end with lots more functions than you begin with. That should mean that each function has to be smaller. And this is a good outcome. Having small functions or methods makes them easy to understand, change and test. And yes, more likely to be reused.

As a rule of thumb, you must pay attention to functions with more than 10 instructions. Has this function only one clear responsibility? Is she doing too much? Can you extract some instructions into a helper low-level function?

You sure do. Keep your functions with 9 or fewer statements; i.e length of 1 digit.

// ❌
function calculateDiscount(booking: Booking) {
  const discount = 0;
  if (booking.passengers.length > 2) {
    discount += 5;
  }
  const payment = getPaymentByBookingId(booking.id);
  if (payment.method === PaymentMethods.Cash) {
    discount += 5;
  }
  const client = getClientById(booking.clientId);
  if (client.isVip) {
    discount += 10;
  }
  const trip = getTripById(booking.tripId);
  const season = getTripSeason(trip);
  if (season === Seasons.Winter) {
    discount += 10;
  }
  const totalDiscount = (discount * booking.price) / 100;
  return totalDiscount;
}

// ✅
function calculateDiscount(tripId: string) {
  const discountPercent = 0;
  const booking = getBookingById(bookingId);
  discountPercent += calculatePassengersDiscount(booking);
  discountPercent += calculatePaymentDiscount(booking);
  discountPercent += calculateClientDiscount(booking);
  discountPercent += calculateSeasonDiscount(booking);
  const discount = (discountPercent * booking.price) / 100;
  return discount;
}
// logical level
function calculatePassengersDiscount(booking: Booking) {
  if (booking.passengers.length > 2) {
    return 5;
  }
  return 0;
}
function calculatePaymentDiscount(booking: Booking) {
  const payment = getPaymentByBookingId(booking.id);
  if (payment.method === PaymentMethods.Cash) {
    return 5;
  }
  return 0;
}
function calculateClientDiscount(booking: Booking) {
  const client = getClientById(booking.clientId);
  if (client.isVip) {
    return 5;
  }
  return 0;
}
function calculateSeasonDiscount(booking: Booking) {
  const trip = getTripById(booking.tripId);
  const season = getTripSeason(trip);
  if (season === Seasons.Winter) {
    return 5;
  }
  return 0;
}

1️⃣ ... good name

Naming is the concrete of clean code buildings. Functions and methods should have a name that describes what they do. Doing means action, so they should contain a verb, be a phrase name, or use a boolean verb for boolean functions.

// ❌
function get() {
  // what is getting?
  return db.select("select * from trips");
}
function trip(id: string) {
  // what is doing?
  return db.select("select * from trips where id=" + id);
}
function cancelled(booking: Booking) {
  // could be clearer?
  return booking.status === BookingStatus.Cancelled;
}

// ✅
function selectTrips() {
  return db.select("select * from trips");
}
function selectTripById(id: string) {
  return db.select("select * from trips where id=" + id);
}
function isCancelled(booking: Booking) {
  return booking.status === BookingStatus.Cancelled;
}

🌅 Conclusion

Clean code is essential to have programs maintained by different programmers that last for years. Functions and methods are the logical building blocks of any software. Keep them small, clear, and well-named. It is easy if you remember the rule of ONE.

In this article, you learned how you can write clean functions. Keep improving to write clean code.



  • 1️⃣ Entry argument
  • 1️⃣ Level of nesting
  • 1️⃣ Level of abstraction
  • 1️⃣ Responsibility
  • 1️⃣ Digit length
  • 1️⃣ Verb in the name

learn, code, enjoy, repeat

Alberto Basalo

Popular posts from this blog

Fine-tune ESLint rules to write better TypeScript

Writing clean code is a lot easier with the right tools well configured. ESLint is a static analyzer for JavaScript programs. So what does that mean and what can it do for my TypeScript code? First things first, by static it means that you don't need to run or even compile a program to parse it. Analyzing means that it checks your code searching for flaws or bad metrics that can cause problems in the long run. The ESLint is a linter that runs before the compiler. Witch runs before your automated tests and the end-user. Is your first line of defense and should be as integrated with your editor as possible. For the details about integrating ESLint and VSCode, you can read my article to configure basic extensions But ESLint would not be so popular if it was not extensible and configurable . And there is the real power and complexity. In this article, I'll walk you through

10 commandments to naming and writing clean code with TypeScript

A code writer is like any other writer; writes to be read and understood . To do so it must obey certain laws. They are so important that must be followed religiously. Being a good writer is an art. But you can be a better programmer by knowing and applying a set of standards. In this guide, I will show you TypeScript samples for the  10 rules of clean naming . When you're finished, you'll be able to write heavenly code.Let there be code naming conventions 1️⃣ Be Descriptive Add value without being repetitive. Context is key. Prefer clarity over saving space. We are not in the ‘90s’ anymore. // ❌ const width = 5 ; class Image { imageWidth = width ; } // ✅ const imageWidth = 5 ; class Image { width = imageWidth; } 2️⃣ Be Meaningful Use the same word for the same concept. Create a dictionary for business and infrastructure common words. // ❌ getClient () {} readProvider () {} postCustomer () {} // ✅ getClient ()

How to configure VSCode to code better TypeScript

Writing a more readable code is your first goal as a programmer. This initial Visual Studio Code setup makes your job easier. Any good worker must know and customize his tools for the job at hand. With minor tweaks, VSCode is the perfect tool to write TypeScript . There are plenty of guides to configure VSCode . Most of them include a lot of extensions. But almost none starts from the very beginning, with no extension at all. I am an enthusiast of writing clean code , so I do my best to promote and facilitate this goal, and I am sure that this can help you to write better code. In this post, you will learn how to adapt VS Code to write better TypeScript code, even without any extension. And above all, you will get tips to adopt good habits and detect programming vices. 🎒 Prerequisites To complete this tutorial, you will need: A local copy of Visual Studi