Write Clean TypeScript with the Rule of ONE for Methods and Functions.

Alberto Basalo
7 min readNov 22, 2022

--

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 the function rules — a rule to rule them all.

functions rule ONE

The code you write is meant to be read several times, so paying attention to how you write it is essential. Especially in places 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.

This article will guide you 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: object,
paymentData: object,
confirmationMessage: object) {
// 🤢 make payment
console.log("making a payment with", paymentData);
// 🤢 save booking
console.log("saving booking", clientBooking);
// 🤢 send a confirmation email
console.log("sending a confirmation email", confirmationMessage);
}

// ✅
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
) {
// 🤢 multiple similar parameters are error-prone
}

// ✅
type Message = {
senderName: string;
senderAddress: string;
recipientAddress: string;
subject: string;
body: string;
}; // 😏 encapsulate variables in types or interfaces
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: any[] = getPassengers();
if (passengers.length >= 0) {
for (let i = 0; i <= passengers.length; i++) {
if (passengers[1].hasAcceptedCommunications) {
if (passengers[i].emailAddress) {
// 🤢 I am lost in the pyramid
console.log("send trip details by email", passengers[i].emailAddress);
}
if (passengers[i].phoneNumber) {
console.log("send trip details by SMS", passengers[i].phoneNumber);
}
}
}
}
}
function getPassengers() {
return [];
}

// ✅
function sendTripDetails() {
const passengers: any[] = getPassengers();
if (passengers.length === 0) return;
// 😏 early return cleans the use case logic
for (let i = 0; i <= passengers.length; i++) {
// 😏 single-line blocks
sendTripDetailsToPassenger(passengers[i]);
}
}
function sendTripDetailsToPassenger(passenger: any) {
if (passenger.hasAcceptedCommunications == false) return;
if (passenger.emailAddress) {
console.log("send trip details by email", passenger.emailAddress);
}
if (passenger.phoneNumber) {
console.log("send trip details by SMS", passenger.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 responsibilities (usually its vocabulary). You know, white collars don`t get their hands dirty. 👔👩🏼‍💻🧑🏼‍🏭

export class Database {
select(query: string) {
return 0;
}
}
const db = new Database();

// ❌
function getAvailablePlaces(tripId: string) {
// 🤢 low-level knowledge exposed
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;
// 🤢 mixed with enterprise rules
const OVERBOOKING_FACTOR = 1.05;
return free * OVERBOOKING_FACTOR;
}

// ✅
// 😏 presentation level (👔 storefront workers)
function getAvailablePlaces(tripId: string) {
const freeSeats = getFreeSeats(tripId);
return calculateAvailable(freeSeats);
}
// 😏 logical level (👩🏼‍💻 administrative workers)
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 (🧑🏼‍🏭 hardcore workers)
function selectTripCapacity(tripId: string): number {
const queryCapacity = "select capacity from trips where tripId=" + tripId;
return db.select(queryCapacity);
}
function selectTripBookedSeats(tripId: string): number {
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.

export class Database {
insertBooking(booking: any) {
// 🤢 mutation❗
}
selectAvailableSeats(tripId: string) {
return 0; // 🤢 question❓
}
}
const db = new Database();

// ❌
function saveBooking(booking: any): 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 mutates any argument’s state.

// ❌
function getDiscountBooking(booking: any): number {
const discount = booking.price * 0.1;
booking.price = booking.price - discount; // mutation❗
return discount; // question like pretending it's not touching anything ❓
}

// ✅
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; // 😏 answer❓
}
function applyDiscount(booking: Booking, discount: number): void {
booking.price = booking.price - discount; // 😏 change❗
}

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. Small functions or methods make them easy to understand, change and test. And yes, more likely to be reused.

What is a digit length? Well, in strict mode, it will be a number between 1 and 9… but can open the rule to a hexadecimal digit. So, ok, less than 16 instructions.

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

You sure do. Keep your functions with 15 or fewer statements, a length of 1 (hex)digit.

function getPaymentByBookingId(bookingId: string) {
return {
method: "cash",
};
}
function getClientById(clientId: string) {
return {
isVip: true,
};
}
function getTripById(tripId: string) {
return {};
}
function getTripSeason(trip: any) {
return "winter";
}
// ❌
function calculateDiscount(booking: any) {
// 🤢 how is this working?
// 🤢 how can I test it?
let discount = 0;
if (booking.passengers.length > 2) {
discount += 5;
}
const payment = getPaymentByBookingId(booking.id);
if (payment.method === "cash") {
discount += 5;
}
const client = getClientById(booking.clientId);
if (client.isVip) {
discount += 10;
}
const trip = getTripById(booking.tripId);
const season = getTripSeason(trip);
if (season === "winter") {
discount += 10;
}
const totalDiscount = (discount * booking.price) / 100;
return totalDiscount;
}

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

1️⃣ … good name

Naming is the concrete foundation 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.

Read more about clean code and naming in the other articles in this series.

🌅 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 could write clean functions. Keep improving to write clean code and get rid of complexity.

dont let complexity nest
  • 1️⃣ Entry argument
  • 1️⃣ Level of nesting
  • 1️⃣ Level of abstraction
  • 1️⃣ Responsibility (query or command)
  • 1️⃣ Digit length (less than 16 lines) and…
  • 1️⃣ Verb in the name

To learn more about cleaning your code, advance to clean your data.

🙏🏼If you liked this post, consider clapping or sharing it. Check my profile to get in contact or find more info about clean code, testing, and best practices with TypeScript.

learn, code, enjoy, repeat

Alberto Basalo

Alberto Basalo, elevating code quality.

--

--

Alberto Basalo

Advisor and instructor for developers. Keeping on top of modern technologies while applying proven patterns learned over the last 25 years. Angular, Node, Tests