A Practical Guide to First-Class Functions Using TypeScript With Node.js
A Practical Guide to First-Class Functions Using TypeScript With Node.js
Learn and implement a core concept of functional programming using TypeScript.
Source: Unsplash
Why First-Class Functions?
Functional programming has been increasingly becoming popular as a programming paradigm. LISP and Haskell are leading the way. However, we, the full-stack developers, are lucky that JavaScript natively supports its features.
One of the core concepts of functional programming is*first-class functions.*Meaning, we are treating functions as first-class citizens. It offers us flexibility in implementing functions by treating them just like any other value (objects, strings, and numbers), passing them as arguments, or returning them from other functions in our code. Understanding first-class functions is an essential foundation for learning and implementing the functional programming paradigm. The end goal is for us to write maintainable, flexible, and scalable code.
In this article, I’m going to walk you through the different features of a first-class function. I will share some examples, sample problems, and solutions. Note: I wrote all my examples in TypeScript and tested them with Node 10.
Arrow Functions — Shorthand
I will use the shorthand for expressing functions in ES6 by default for the rest of this article. So that our code will be more concise, let’s set the tone by reviewing and understanding how to write arrow functions in ES6. Feel free to skip this part if you are already familiar with arrow functions.
We would typically use thefunction
keyword when we wrote functions before ES6, for example, if we wanted to write a function that returns the winning percentage. We wrote the function and named it, or we assigned it to a variable by usingconst
,var
, orlet
.
Named functions in TypeScript
Starting with ES6, we can rewrite our function to an arrow function as in the example below.
Arrow function introduced in ES6
We do not need to write thefunction
keyword. The arrow=>
is pointing to the set of instructions in our function enclosed in curly braces. In TypeScript, we can also specify that a variable is aFunction
type if we want to.
The example below returns the result directly without keeping it in our variable. This further shortens our code.
One-liner with an arrow function
Notice how the arrow=>
above tells the interpreter that we want to return the value of the statement it’s pointing to, which is the result when we dividematches
bywins
, as long as we don’t enclose the statement in curly braces.
We need to enclose our arguments in parentheses in TypeScript if we specify the type, unlike when using JavaScript arrow functions. We can omit the parentheses if we only need to pass one argument.
Arrow function with one typed argument
Use an empty parenthesis if there is no argument passed.
Arrow function without an argument
If we want to return an object, enclose it in parentheses. If we don’t enclose it in parentheses, we tell the interpreter that we are using the brackets to define a function’s body.
Enclose the object in parenthesis if you want to return an object.
Writing functions in this shorthand form allows us to “compress” our code, saving us from extra keystrokes when typing and additional characters when reading. Writing functions in shorthand form can be a little confusing in the beginning; however, there’s compounding interest to gain here in the long term. Aside from saving us from extra keystrokes and redundant use of the wordfunction, it makes it convenient for us to write our TypeScript code in a functional programming style. How? Arrow functions resemble components of a mathematical equation, and that keeps our code concise and less cluttered.
Treating Functions as Data
We can treat functions the same as we treat other TypeScript data types, like numbers, strings, arrays, and objects. We can store a function in a variable and treat it just like any other TypeScript data type, which gives us a few useful options for what we can do with our functions.
Here’s an example of assigning our function to another variable.
Create a function and assign it to another variable.
Treating functions like the other data types also allows us to change the behavior of a function dynamically.
Use a ternary operator to change the function behavior dynamically.
Here are some typical real-world applications of this feature.
Example usage 1. Mocking in unit tests
One common application of this feature is to mock what our async function returns when running unit tests locally.
Example usage 2. A/B testing or experimentation
We can also assign our functions to an object to form a hash map or dictionary of functions that we can use for our A/B tests.
Example usage 3. Execute a series of functions to a data
Here’s another interesting application. We have many functions that take an object, execute a series of updates to that object, and return an updated object.
const addName = (person: object): object => ({
...person,
name: 'John Doe'
})
const addGender = (person: object): object => ({
...person,
gender: 'Male'
})
const addJobTitle = (person: object): object => ({
...person,
title: 'Software Engineer'
})
const addMonthlySalary = (person: object): object => ({
...person,
monthlySalary: 1000
})
const addAnnualSalary = (person: object): object => ({
...person,
annualSalary: person['monthlySalary'] * 12
})
The procedural way to do this is to apply the function line by line. For example, we have anemployee
object initialized with anid
. Then a series of functions will update theemployee
object and return the updatedemployee
object. See the example code below.
Take the person object and update it.
The code above gets the job done. We are applying each function line by line to the mutatingemployee
object. Imagine if we need to add a new function: Add a new line to call the function and pass theemployee
object as an argument. Not bad, but we can do better.
The functional way is to assign the functions in an array, iterate through that array usingforEach
, and apply each function to the argument. Note theemployee
argument to pass after applying the first function in the array will be the returned value of the previous function. We use the functions in the same order they are positioned in the array.
Take the person object and update it using an array of functions.
If we need to add another function to execute, we only need to add another entry in the array. We do not need to reassign the returned value toemployee
and pass it as an argument to the next function.
Functions as Arguments
Going back to the theme of dynamically changing a function’s behavior, let’s look at how to achieve that by passing functions as arguments.
For example, we want to implement a simple calculator that accepts two numbers. The object-oriented approach is to create aCalculator
class that contains the calculation operations as methods within that class, which areadd
andsubtract
. If we need to add more operations, we will update the class to add each operation’s corresponding method. Another way is to write an individual function for each operation. For instance, implement thecalculateSum()
andcalculateDifference()
functions.
The “first-class function” or “functional” way to implement the calculator here will be to create acalculate()
function that accepts an operation function, e.g.add()
, as an argument that will perform the calculation we want. The function that calculates is also self-contained, and we can use them without them being passed as an argument.
Pass the operation function as an argument to the calculate function.
Functions as Return Values
Seeing code that returns a function from another function can be confusing in the beginning. It helps to think of it as a factory of functions.
An easier way to understand this concept is to start with a function that returns another function that prints text to the console. The function below returns a function that prints “hello” to the console.
const createGreeting = () => () => console.log('hello')
It might take some time to get used to reading this type of function if you’re new to this. Keep in mind that this function is the equivalent of the following function.
const createGreeting = function() {
return function() {
console.log('hello')
}
}
Let’s look at an example. We have a program with conversion functions that convertmeter
into the imperial unitsmile
,yard
,foot
andinch
. These unit conversion functions take the valuex
in meter and return the converted value in an imperial unit formatted with its corresponding symbol.
Conversion functions with repetitive code
The duplicated code in our conversion functions is quite evident in the example above. Multiplication and string formatting in each function is repetitive.
Let’s try to solve this by creating a function that will handle the different conversions for us while maximizing code reuse between functions with slightly different behaviors.
ThecreateConverter()
function below accepts the conversion valuev
and symbols
arguments. It returns a function that takes anx
argument, which is the value that we want to convert.
const createConverter = (v: number, s: string) => (x: number) => `${x * v} ${s}`
We will then reuse thecreateConverter
function above to form our conversion functions.
const meterToMile = createConverter(0.0006213689, 'mi')
console.log(meterToMile(1000))
Here is our full solution below. We are maximizing reuse while keeping our code maintainable.
Conversion functions with reusable code. We are utilizing functions as return values.
Closures
Since we’ve covered returning a function from another function, we should understand the variables’ scope within the function that returns a function. The returning function’s variables are accessible within the returned function.
The variable_name
in thecreateGreeting()
function below is accessible from the returned function that prints to the console. It will print outHello John Doe
when we run it.
Function with closure
Private Variables
This section will combine what we have learned about returning functions and closures in the previous sections. Usually, in JavaScript, we will append our private variable with an underscore to indicate that we should not use the variable anywhere else outside its class. Sometimes this can be ignored or misused. We can enforce how a class in object-oriented programming handles private variables by using a functional approach. Instead of usingclass
in our example, we will implement it as a function.
Take a look at the example below. We have anEmployee
function that accepts thename
,jobTitle
,andmonthlySalary
arguments. The function assigns these arguments to the private variables appended with an underscore to indicate that they are private. We can only access these variables through our getters:getName()
,getJobTitle()
, andgetEmployee()
. If we attempt to retrieve the private variableemployeeA._name
directly, we will get aProperty ‘_name’ does not exist
error.
Employee function with private variables and getters
We can also expand ourEmployee
function to use setters. We have added the setters in the example below.
Private variables with setters
The last two lines in the code above will output our employee’s updated job title and salary.
Please note: You do not always need to write your “class” like this in TypeScript because TypeScript already supports the implementation of private variables in a class. It’s good to know that you have the option to implement your private variables this way.
Higher-Order Functions
A higher-order function is a function that operates on other functions by taking any function as an argument or by returning them. Let’s go through an example of how we can use higher-order functions to give us flexibility and reusability in our code.
To illustrate this in an example, let’s take a look at a common problem that we solve as programmers: validating our function arguments.
We have a function that performs a subtraction operation, given aminuend
and asubtrahend
as its arguments, and returns thedifference
.
const minus = (minuend: number, subtrahend: number): number => minuend - subtrahend
Minuend — This is the biggest number, or the whole, from which apartwill be taken away.
Subtrahend — This is thepartthat is taken away from the minuend.
Difference — This is thepartthat is left-over aftersubtraction. [1]
Let’s try to limit it to return non-negative numbers only. Therefore we will need our arguments such that theminuend
should always be greater than or equal to thesubtrahend
:minuend >= subtrahend
.
The typical way to validate the arguments will be to add an early return or exit on top of the function.
const minus = (minuend: number, subtrahend: number): number => { if (minuend < subtrahend) { console.log('minuend should be greater than the subtrahend') return null } return minuend - subtrahend}
The code above works. However, the function is doing more than one thing. Instead of returning the difference only, it also validates the arguments. It violates the single-responsibility principle in S.O.L.I.D.[2]
Let’s solve that by using higher-order functions.
First, we’ll create a separate function for validating the arguments.
const minuendGreaterThanSubtrahend = (func: Function) => (...args) => { const [num1, num2] = args if (num1 < num2) { console.log('minuend should be greater than the subtrahend') return null } return func(...args) }
The functionminuendGreaterThanSubtrahend()
accepts a function passed as an argument. It then validates the arguments that will be called from the function passed.
Let’s remove the validation in ourminus()
function, getting it back to its original form that complies with the single-responsibility principle.
const minus = (minuend: number, subtrahend: number): number => minuend - subtrahend
We will then create theminusPositiveDifference()
function that calls theminuendGreaterThanSubtrahend()
validation function with theminus()
function as its argument. ThisminusPositiveDifference()
will then be called to ensure that our arguments are validated first before performing the subtraction.
const minusPositiveDifference = minuendGreaterThanSubtrahend(minus)
See our full solution below. Each function is doing one thing while still achieving the argument validation that we want.
A functional approach to validate arguments while keeping our code compliant with the single-responsibility principle
Conclusion
Understanding first-class functions in JavaScript (TypeScript) is crucial to your functional programming journey. It helps us approach problems in exciting ways while utilizing the power and flexibility of first-class functions. You are going to encounter problems with patterns similar to the examples in this article. The more you use these first-class function features in your projects, the more you’ll master them.
References
If you want to run the sample code in this article, it is also available onGitHub.