DEV Community

Cover image for Easy functional programming techniques in TypeScript for everyone
Deepu K Sasidharan
Deepu K Sasidharan

Posted on • Updated on • Originally published at deepu.tech

Easy functional programming techniques in TypeScript for everyone

Originally published at deepu.tech.

There is a lot of hype around functional programming(FP) and a lot of cool kids are doing it but it is not a silver bullet. Like other programming paradigms/styles, functional programming also has its pros and cons and one may prefer one paradigm over the other. If you are a TypeScript/JavaScript developer and wants to venture into functional programming, do not worry, you don't have to learn functional programming oriented languages like Haskell or Clojure since JavaScript and hence TypeScript has you covered and this post is for you.

If you are looking for functional programming in Java or Golang check other posts in the series.

I'm not gonna dive into all functional programming concepts in detail, instead, I'm gonna focus on things that you can do in TypeScript which are in line with functional programming concepts. I'm also not gonna discuss the pros and cons of functional programming in general.

Please keep in mind, though this post is about TypeScript, you can easily do the same in JavaScript as well since TypeScript is just a typed superset of JavaScript.


What is functional programming?

As per Wikipedia,

Functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.

Hence in functional programming, there are two very important rules

  • No Data mutations: It means a data object should not be changed after it is created.
  • No implicit state: Hidden/Implicit state should be avoided. In functional programming state is not eliminated, instead, its made visible and explicit

This means:

  • No side effects: A function or operation should not change any state outside of its functional scope. I.e, A function should only return a value to the invoker and should not affect any external state. This means programs are easier to understand.
  • Pure functions only: Functional code is idempotent. A function should return values only based on the arguments passed and should not affect(side-effect) or depend on global state. Such functions always produce the same result for the same arguments.

Apart from these there are functional programming concepts below that can be applied in TypeScript, we will touch upon these further down.

Using functional programming doesn't mean its all or nothing, you can always use functional programming concepts to complement Object-oriented concepts in TypeScript. The benefits of functional programming can be utilized whenever possible regardless of the paradigm or language you use. And that is exactly what we are going to see.


Functional programming in TypeScript

TypeScript is not a purely functional language but offers a lot of concepts which are in line with functional languages, so let us see how we can apply some of the functional programming concepts above in TypeScript.

First-class and higher-order functions

First-class functions(function as a first-class citizen) means you can assign functions to variables, pass a function as an argument to another function or return a function from another. TypeScript supports this and hence makes concepts like closures, currying, and higher-order-functions easy to write.

A function can be considered as a higher-order-function only if it takes one or more functions as parameters or if it returns another function as a result.

In TypeScript, this is quite easy to do

type mapFn = (it: string) => number;

// The higher-order-function takes an array and a function as arguments
function mapForEach(arr: string[], fn: mapFn): number[] {
    const newArray: number[] = [];
    arr.forEach(it => {
        // We are executing the method passed
        newArray.push(fn(it));
    });
    return newArray;
}

const list = ["Orange", "Apple", "Banana", "Grape"];

// we are passing the array and a function as arguments to mapForEach method.
const out = mapForEach(list, (it: string): number => it.length);

console.log(out); // [6, 5, 6, 5]
Enter fullscreen mode Exit fullscreen mode

But then in JavaScript/TypeScript we could also simply do it this way using built-in functional methods like map, reduce and so on.

const list = ["Orange", "Apple", "Banana", "Grape"];

// we are passing a function as arguments to the built-in map method.
const out = list.map(it => it.length);

console.log(out); // [6, 5, 6, 5]
Enter fullscreen mode Exit fullscreen mode

Closures and currying are also possible in TypeScript

// this is a higher-order-function that returns a function
function add(x: number): (y: number) => number {
    // A function is returned here as closure
    // variable x is obtained from the outer scope of this method and memorized in the closure
    return (y: number): number => x + y;
}

// we are currying the add method to create more variations
var add10 = add(10);
var add20 = add(20);
var add30 = add(30);

console.log(add10(5)); // 15
console.log(add20(5)); // 25
console.log(add30(5)); // 35
Enter fullscreen mode Exit fullscreen mode

There are also many built-in declarative higher-order-functions in TypeScript/JavaScript like map, reduce, forEach, filter and so on. There are also many libraries that provide functional interfaces to be used in TypeScript/JavaScript.

Pure functions

As we saw already a pure function should return values only based on the arguments passed and should not affect or depend on global state. It is possible to do this in TypeScript easily.

This is quite simple, take the below this is a pure function. It will always return the same output for the given input and its behavior is highly predictable. We can safely cache the method if needed.

function sum(a: number, b: number): number {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

If we add an extra line in this function, the behavior becomes unpredictable as it now has a side effect that affects an external state.

const holder = {};

function sum(a: number, b: number): number {
    let c = a + b;
    holder[`${a}+${b}`] = c;
    return c;
}
Enter fullscreen mode Exit fullscreen mode

So try to keep your functions pure and simple. Using tools like ESLint and typescript-eslint it is possible to enforce these.

Recursion

Functional programming favors recursion over looping. Let us see an example for calculating the factorial of a number.

In traditional iterative approach:

function factorial(num: number): number {
    let result = 1;
    for (; num > 0; num--) {
        result *= num;
    }
    return result;
}

console.log(factorial(20)); // 2432902008176640000
Enter fullscreen mode Exit fullscreen mode

The same can be done using recursion as below which is favored in functional programming.

const factorial = (num: number): number =>
    num == 0 ? 1 : num * factorial(num - 1);

console.log(factorial(20)); // 2432902008176640000
Enter fullscreen mode Exit fullscreen mode

The downside of the recursive approach is that it will be slower compared to an iterative approach most of the times(The advantage we are aiming for is code simplicity and readability) and might result in stack overflow errors since every function call needs to be saved as a frame to the stack. To avoid this tail recursion is preferred, especially when the recursion is done too many times. In tail recursion, the recursive call is the last thing executed by the function and hence the functions stack frame need not be saved by the compiler. Most compilers can optimize the tail recursion code the same way iterative code is optimized hence avoiding the performance penalty. Tail call optimization is part of the ECMAScript specs but unfortunately, most JavaScript engines do not support this yet.

Now using tail recursion the same function can be written as below, but depending on the engine this might not be optimized, though there are workarounds, still it performed better in benchmarks.

const factorialTailRec = (num: number): number => factorial(1, num);

const factorial = (accumulator: number, val: number): number =>
    val == 1 ? accumulator : factorial(accumulator * val, val - 1);

console.log(factorialTailRec(20)); // 2432902008176640000
Enter fullscreen mode Exit fullscreen mode

Consider using recursion when writing TypeScript code for readability and immutability, but if performance is critical or if the number of iterations will be huge use standard loops.

Lazy evaluation

Lazy evaluation or non-strict evaluation is the process of delaying evaluation of an expression until it is needed. In general, TypeScript does strict/eager evaluation but for operands like &&, || and ?: it does a lazy evaluation. We can utilize short-circuiting, higher-order-functions, closures, and memoization techniques to do lazy evaluations.

Take this example where TypeScript eagerly evaluates everything.

function add(x: number): number {
    console.log("executing add"); // this is printed since the functions are evaluated first
    return x + x;
}

function multiply(x: number): number {
    console.log("executing multiply"); // this is printed since the functions are evaluated first
    return x * x;
}

function addOrMultiply(
    add: boolean,
    onAdd: number,
    onMultiply: number
): number {
    return add ? onAdd : onMultiply;
}

console.log(addOrMultiply(true, add(4), multiply(4))); // 8
console.log(addOrMultiply(false, add(4), multiply(4))); // 16
Enter fullscreen mode Exit fullscreen mode

This will produce the below output and we can see that both functions are executed always

executing add
executing multiply
8
executing add
executing multiply
16
Enter fullscreen mode Exit fullscreen mode

We can use higher-order-functions to rewrite this into a lazily evaluated version

function add(x: number): number {
    console.log("executing add");
    return x + x;
}

function multiply(x: number): number {
    console.log("executing multiply");
    return x * x;
}

type fnType = (t: number) => number;
// This is now a higher-order-function hence evaluation of the functions are delayed in if-else
function addOrMultiply(
    add: boolean,
    onAdd: fnType,
    onMultiply: fnType,
    t: number
): number {
    return add ? onAdd(t) : onMultiply(t);
}
console.log(addOrMultiply(true, add, multiply, 4));
console.log(addOrMultiply(false, add, multiply, 4));
Enter fullscreen mode Exit fullscreen mode

This outputs the below and we can see that only required functions were executed

executing add
8
executing multiply
16
Enter fullscreen mode Exit fullscreen mode

Or by memoization like this

const cachedAdded = {};
function add(x: number): number {
    if (cachedAdded[x]) {
        return cachedAdded[x];
    }
    console.log("executing add");
    const out = x + x;
    cachedAdded[x] = out;
    return out;
}

const cachedMultiplied = {};
function multiply(x: number): number {
    if (cachedMultiplied[x]) {
        return cachedMultiplied[x];
    }
    console.log("executing multiply");
    const out = x * x;
    cachedMultiplied[x] = out;
    return out;
}

function addOrMultiply(
    add: boolean,
    onAdd: number,
    onMultiply: number
): number {
    return add ? onAdd : onMultiply;
}

console.log(addOrMultiply(true, add(4), multiply(4))); // 8
console.log(addOrMultiply(false, add(4), multiply(4))); // 16
Enter fullscreen mode Exit fullscreen mode

This outputs the below and we can see that functions were executed only once for the same values

executing add
executing multiply
8
16
Enter fullscreen mode Exit fullscreen mode

Please note that memoization techniques will work only when your functions are pure and referentially transparent.

There are also other ways of doing Lazy evaluations like this. Doing Lazy evaluations in TypeScript might not be worth the code complexity some of the times, but if the functions in question are heavy in terms of processing then its is absolutely worth it to lazy evaluate them.

Type system

TypeScript has a strong type system and also has great type inference. While the underlying JavaScript itself is weakly typed, TypeScript along with a compatible IDE can bridge that gap.

Referential transparency

From Wikipedia:

Functional programs do not have assignment statements, that is, the value of a variable in a functional program never changes once defined. This eliminates any chances of side effects because any variable can be replaced with its actual value at any point of execution. So, functional programs are referentially transparent.

Unfortunately, there are not many ways to strictly limit data mutation in JavaScript, however by using pure functions and by explicitly avoiding data mutations and reassignment using other concepts we saw earlier this can be achieved. JavaScript by default passes primitive variables by value and objects by reference so we need to take care not to mutate data inside functions. Libraries like Immutable JS could also be considered. Use const as much as possible to avoid reassignments.

For example, the below will produce an error

const list = ["Apple", "Orange", "Banana", "Grape"];

list = ["Earth", "Saturn"];
Enter fullscreen mode Exit fullscreen mode

But this will not help when variables are holding references to other objects, for example, the below mutation will work irrespective of the const keyword.

const list = ["Apple", "Orange", "Banana", "Grape"];

list.push("Earth"); // will mutate the list
list.push("Saturn"); // will mutate the list
Enter fullscreen mode Exit fullscreen mode

const keyword allows the internal state of referenced variables to be mutated and hence from a functional programming perspective const keyword is useful only for primitive constants and to catch reassignments.

However, with TypeScript, we can use special mapped types to make objects read-only and hence avoiding accidental data mutations which are caught during compile time. Thanks to @stereobooster and @juliang for pointing it out. Read my post about mapped and conditional types here to learn more.

const list: Readonly<string[]> = ["Apple", "Orange", "Banana", "Grape"];

list.push("Earth"); // will cause compilation error
Enter fullscreen mode Exit fullscreen mode

or

const list: ReadonlyArray<string> = ["Apple", "Orange", "Banana", "Grape"];

list.push("Earth"); // will cause compilation error
Enter fullscreen mode Exit fullscreen mode

Other techniques to follow are using Object.freeze or built-in methods like map, reduce, filter and so on as they do not mutate the data. We can also use this ESlint plugin to restrict mutations.

Data structures

When using functional programming techniques it is encouraged to use data types such as Stacks, Maps and Queues which have functional implementations as well.
Hence maps are better than arrays or hash sets in functional programming as data stores.


Conclusion

This is just an introduction for those who are trying to apply some functional programming techniques in TypeScript. There are a lot more that can be done in TypeScript and with the ever-evolving ECMAScript underneath, this should be even easier. As I said earlier functional programming is not a silver bullet but it offers a lot of useful techniques for more understandable, maintainable and testable code. It can co-exist perfectly well with imperative and object-oriented programming styles. In fact, we all should be using the best of everything.


I hope you find this useful. If you have any question or if you think I missed something please add a comment.

If you like this article, please leave a like or a comment.

You can follow me on Twitter and LinkedIn.

Top comments (17)

Collapse
 
aminnairi profile image
Amin

Hi there, great article! Did you know that you can use Object.freeze to prevent mutating an array or an object?

const list = Object.freeze([])

list.push(1)
// Throws an error
Collapse
 
macsikora profile image
Pragmatic Maciej

It will couse runtime exception. So Object.freeze is not useful in terms of TS.

Collapse
 
aminnairi profile image
Amin

Hi Maciej. You are absolutely right! That's why it is so good coupled with TypeScript which can prevent mutating a freezed object by refusing to transpile the file.

Thread Thread
 
macsikora profile image
Pragmatic Maciej

Still I see readonly as a tool for the job. Object.freeze is fully run-time thing and it is decoupled from type definition. Declaration of readonly is included into type definition, so it is explicit. I don't think run-time block is needed here.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Yes, I thought of writing about it but then missed

Collapse
 
stereobooster profile image
stereobooster • Edited

Unfortunately, there are not many ways to strictly limit data mutation in TypeScript

type Foo = {
  bar: number;
  bas: number;
}
type FooReadonly = Readonly<Foo>; 

or

const list = ["Apple", "Orange", "Banana", "Grape"] as const;

Mutation doesn't affect referential transparency - reference stays the same.

const test = {}
test.a = 1
test === test
Collapse
 
deepu105 profile image
Deepu K Sasidharan

I have updated the post

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Yes, I missed that

Collapse
 
juliang profile image
Julian Garamendy

Hi! It may be worth mentioning that
"TypeScript comes with a ReadonlyArray type that is the same as Array with all mutating methods removed, so you can make sure you don’t change your arrays after creation"

typescriptlang.org/docs/handbook/i...

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Oh yes, I completely forgot about those

Collapse
 
macsikora profile image
Pragmatic Maciej

functional data types such as Stacks, Maps and Queues.

Stack, Map and Queue are not data types related to FP, they have FP implementations, but still the sentence is wrong.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

I have rephrased it

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Sure they are not related to FP, what I meant was that they are more appropriate for the FP approach

Collapse
 
bobmyers profile image
Bob Myers

Virtually everywhere you say "TypeScript", you seem to mean "JavaScript". It is not TypeScript that treats functions as first-class objects, or lazily evaluates boolean expressions. It's JavaScript. TypeScript is merely JavaScript with type annotations.

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Yes, and thats why I keep saying typeScript/JavaScript and also I made it clear in the beginning that TS is just a superset of JS

Collapse
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard

What kind of markdown do you use to link to the other articles at the top? It looks neat!

Collapse
 
deepu105 profile image
Deepu K Sasidharan

Its the series feature in Dev, if you add series: my series name to the front matter of the post-it links them up that way