Functional programming in Typescript
scroll

Functional programming in Typescript

 

Even though functional programming is not a new concept, it has seen strong recent adoption in modern UI frameworks like React and Redux, complementing or even fully replacing OOP style.

The expressive type system from Typescript is a perfect match to functional programming, leveraging its already large advantages that include simpler code, easier debugging, testing, and concurrent programming.

In this article, we’ll explore the relationship between this functional programming style and type system and, without going too deep with technicalities, show the practical applications in a pragmatic and hands-on way.

Pure functions

Function purity is the cornerstone of functional programming. In simple terms, a pure function doesn’t have any hidden inputs or outputs and always returns the same output for the same input.

This makes it perfect for unit testing since you can always expect the same behavior for a given test data. Also, this predictability makes it easier to go about them thinking like they are lego blocks.

Some things that make a function impure include:

  • API calls
  • Access to the DOM, window, document
  • Writing or reading external variables
  • Using timers, intervals, requestAnimationFrame
  • Subscribing to events 

Examples of impure and pure functions

// Impure functions:
const now = () => new Date().valueOf();
let count = 0;
const increment = () => count++;
const rand = () => Math.random();
const url = () => document.location.href;
 
// Pure functions:
const concat = (a: string, b: string) => a + b;
const isOdd = (x: number) => x % 2 == 1;
 
const filterOdds = (x: number[]) => x.map(isOdd);

Semantic purity

Even when a function always returns a different instance, you can consider it as a pure function if, for its intended purpose, you only care about the output content and not about its referential identity.

This is the case with filterOdds shown above and also with React components since React components always return a different instance for each render. Still, you need to consider them as pure since React only cares about the content and not the instance itself.

It’s easy to make a function like this strictly pure by using memoization which is simply to store its output and return it again when the same input is used.

As of version 4.2.3, Typescript doesn’t have a way to fully enforce function purity, but you can use the Readonly<> type to ensure that no function alters an object field. In addition, you can use const instead of let or var to enforce immutability at the variable level.

Function composition

These kinds of functions are very restricted. The only thing that a pure function can do is to take an input, transform it in some way, and return an output. 

By definition, these functions can’t access any external state, so they are easy to refactor and compose since you only have to think in terms of its explicit inputs and outputs.

Therefore, they can be used freely in concurrent programming since it’s impossible for a function to cause any side effect or to depend on something outside its definition, because of this, the order of execution doesn’t matter.

Due to these self-imposed limitations, pure functions are often composed in two simple ways - pipelines or trees:

Pipeline composition example

Functional programming in Typescript

A notable example of tree composition is the reducer function in redux as it works with a state tree. Here, the reducer logic can be distributed in multiple smaller functions that operate only over a certain part of the tree.

Function composition in Typescript

Thanks to type expressions, you can do function composition in a type-safe manner meaning the compiler can verify only the correct input types are used and infer correct output types.

Consider the following example:

const arr: (number | string)[] = [1, 2, "hello there"];
const isString =
   (x): x is string => typeof x == "string";
const split = (x: string) => x.split(" ");
 
const arrayOfWords = arr
   .filter(isString)
   .map(split)
   ;

Note the split function only works with a string input, and thanks to the isString, the element type is narrowed from (number | string) to string. As a result, the split function can be used inside the map call.

On the other hand, if you didn’t correctly filter out numbers from the array, the compiler would complain about a “type not assignable to” error.

Less freedom = easier coding

The purpose of Typescript is to give developers less freedom by decreasing the possible values that an expression can take.

In a similar way, functional programming’s purpose is also to restrict the developer by having a clear separation between data (or state) and functions (or behavior).

These restrictions make the code safer and easier to write, read and test while giving you better tooling because of the improved static analysis that the compiler can do.

FP + TS restricts functions, data, and types

Functional programming in Typescript

Show me the code!

A common problem in web applications is form validation. You can solve it by employing functional programming in Typescript where you will have no choice but to write the correct validation logic in each form.

Consider a customer:

type Customer = {
   name: string;
   age: number;
}

You want a general solution for any form, so you write a type for a function that can validate a single field, returning an error message or null if the field is ok

type Rule<T> = (x: T) => null | string;

Here, you will be using several typescript goodies:

  • Generics, a type that takes another one as an input.
  • Function types, types that represent a function, not a value.
  • Unions, where the function can return either null or string.

Write some functions that match the Rule type. Please note that rules are pure functions.

const ageOfMajority: Rule<number> =
   x => x < 18 ? "Too young" : null;
 
const required: Rule<any> =
   x => !x ? "Required field" : null;

As you can see, each validation rule returns an error message or null if the value is valid, type inference gives x the rule parameter type.

Note that the required rule works with any field type, you can be sure to define a validation rule for each field with mapped types:

type FormRules<T> = {
   [K in keyof T]: Rule<T[K]>
};

FormRules<T> is a type that has a validation rule for each field in T, and each rule has the type parameter of that specific field T[K]

Now, you can define all validation rules per field on the customer type:

const rules: FormRules<Customer> = {
   name: required,
   age: ageOfMajority
}

The beauty of this solution is that there is no way to write an invalid rules object. A rule has to be defined for each customer's field and they only accept a rule that matches the field type.

In any other case, you will get a compiler error.

Web Solutions

Higher-order functions and function composition

Take a minute to consider the following function:

// Combine two rules
const compose =
   <T>(a: Rule<T>, b: Rule<T>): Rule<T> =>
       x => a(x) || b(x);

Here, you just created a function that creates a rule from two other rules! Check the result expression; if rule a returns an error, that is the rule result, otherwise, rule b is evaluated.

Next, you can define each rule as a combination of other rules:

const rules: FormRules<Customer> = {
   name: compose(required, fullName),
   age: compose(ageOfMajority, notTooOld)
}

You could use the reduce function to repeatedly apply compose, thus making it easier to combine more than two rules:

const rules: FormRules<Customer> = {
   name:
       [
           required,
           fullName,
           noNumbersAllowed
       ].reduce(compose),
   age: compose(ageOfMajority, notTooOld)
}

You now have a simple and type-safe framework for field validation on any form.

The imperative shell

Of course, a program made only with pure functions cannot serve any purpose since it wouldn’t have any communication with the outside world. Impure functions have to go somewhere.

Enter the functional core, imperative shell approach where you can do as much work as possible in a functional way, and isolate impure interactions on a thin, outer layer.

The most prominent example of this approach is ReactJS where:

  • All components can be seen as pure functions that take props, state, and context as an input and return a React element as an output.
  • All “outside world interactions” take place on the ReactDOM render/hydrate function.

For API calls, if you use a library like @apollo/client, this is already taken care of in a similar way that ReactDOM takes care of DOM rendering by having a context or hook that abstracts away the impure API call interaction.

This approach is applicable at any scale, in the same way, that React does it on an application level, you can write any impure function by following the same philosophy of isolating interactions and doing most of the work in a functional way.

Outside world interaction 

Functional programming in Typescript

Is Functional Programming opposed to OOP?

Functional programming style can coexist in harmony with any OOP project because you can use pure functions, immutability, and imperative shell principles at any level, like in-class methods and properties.

In addition, you can migrate to functional programming by gradually removing behavior from classes until you end up with data-only immutable classes and functions that operate over classes.

Still, there is a fundamental contradiction between functional programming and OOP that you need to consider:

  • In functional programming, you separate state and behavior. 
  • In OOP, both live in the same place, as objects.

Conclusion

Functional programming is a powerful tool for reducing complexity in large and small projects and can be greatly leveraged with Typescript, enabling you to write safer and easier code while having a better tooling and debugging experience.

This paradigm is becoming more and more popular with new frameworks based on it, like React and Redux. In addition, many other languages are adopting functional programming principles in their design, like Rust, Swift, and C#.

Some concepts like immutability, monads, currying, and referential transparency were left outside the scope of this article to make it more practical. Still, we encourage you to learn more about them to take full advantage of this paradigm.

image
by Rafael Salguero Iturrios

Related articles

article
https://svitla.com/blog/javascript-vs-typescript
JavaScript vs TypeScript
by Svitla Team
January 26, 2021
article

Let's discuss your project

We look forward to learning more and consulting you about your product idea or helping you find the right solution for an existing project.

Thank you! We will contact very shortly.

Your message is received. Svitla's sales manager of your region will contact you to discuss how we could be helpful.