FP & TS chapter 3: Putting fun in functional operators

Published: 18-04-2025 webdevelopment

When you have been developing in TypeScript for a while, you are probably already using the functional operators, even if you don’t realize it.

The operators Array.prototype.map, Array.prototype.filter, and Array.prototype.reduce are native JavaScript methods that are used to iterate over collections without mutating the original collection.

Let’s first look at imperative style iteration. It’s possible to iterate over different types of objects, e.g.

  • string: const x = "123";
  • array: const x = [1, 2, 3];
  • array-like objects: e.g. NodeList const x = document.querySelectorAll('div');
  • Map, Set: const x = new Set([1,2,3]);

The oldest way in JavaScript to iterate is with the for statement, e.g. to iterate over the characters in a string:

const str = "123";
for (let i = 0; i < str.length; i++) {
    console.log(str[i]);
}

This also works for Arrays, but to use it with Sets, you would first have to convert it to an Array. Also, this is quite verbose, because you have to manually define the counter i. Alternatively, the shorthand for...of can be used to solve both issues:

const x = new Set([1,2,3]);
for (const elem of x) {
  console.log(elem);
}

The map operator

With immutability in mind, what is wrong with this example?

const sourceArr = [1,2,3];
const squareArr = [];
for( const item of sourceArr ) {
    squareArr.push(item * item);
}
console.log(squareArr);

The squareArr is mutated, and we want to avoid that it is unexpectedly manipulated. Let’s encapsulate the iteration in a function, so squareArr can’t be mutated outside that function:

const sourceArr = [1,2,3];

const mapSquare = (arr) => {
    const squareArr = [];
    for( const item of arr ) {
        squareArr.push(item * item);
    }
    return squareArr;
}

console.log(mapSquare(sourceArr));

That’s much safer! Now we can also build a more abstract function that can iteratively apply any function, instead of always squaring the value.

const sourceArr = [1,2,3];

const map = (arr, fn) => {
    const resultArr = [];
    for( const item of sourceArr ) {
        resultArr.push(fn(item));
    }
    return resultArr;
}

const square = item => item * item;

console.log(map(sourceArr, square));

The map function returns a value, unlike the for...of loop from the initial example. The function map is a higher-order function, because it takes a function fn as a parameter. The function square is a pure function, and is trivial to unit test.

Although it’s educational to see how we could implement the map function, it’s natively supported in TypeScript (with Array.prototype.map) as well as in many utility libraries like Lodash.

const sourceArr = [1,2,3];
const square = item => item * item;
console.log(sourceArr.map(x => square(x)));

Because Array.prototype.map expects a function with the iterated item as a parameter, and square is a function with an item as a parameter, the map call can be reduced to:

console.log(sourceArr.map(square));

The conclusion that these two expressions are equivalent, is proven by what is called “equational reasoning”.

The map function can also be explicitely typed to expect to return a number. That implies that the result of the map call is always expected to be an array of numbers.

const sourceArr: number[] = [1, 2, 3];
const square = (item: number): number => item * item;
console.log(sourceArr.map<number>(square));

The filter operator

Let’s say you only want to keep the odd numbers from an array. With a for...of loop you could write it similar to the abstract map example above, like:

const sourceArr = [1, 2, 3, 4, 5, 6];

const filter = (arr, predicateFn) => {
    const resultArr = [];
    for( const item of arr ) {
        if (predicateFn(item)) {
            resultArr.push(item);        
        }
    }
    return resultArr;
}

const isOdd = item => item % 2 === 1;

console.log(filter(sourceArr, isOdd));

In this example isOdd is a predicate function. A predicate is a function that always results in true or false and can therefore be used as a condition.

You might realize how to use Array.prototype.map to write this without the for loop, but it’s easier to use the Array.prototype.filter operator. Where map always returns the same amount of items that goes in, filter uses a predicate to determine for each item in the array if it should be returned. The output of filter is therefore of the same length or shorter than its input.

const sourceArr = [1, 2, 3, 4, 5, 6];
const isOdd = item => item % 2 === 1;
console.log(sourceArr.filter(isOdd));

Type guards

In the map example the call used a generic to specify the return type with .map<number>. This does not work the same way with filter, because you can actually narrow the type based on the condition. E.g. if you are filtering odd numbers, numbers go in, but a specific subset of numbers comes out. This is extremely powerful because the type system can help you with this guarantee that the filtered values are of a specific subtype. We use TypeScript’s type guard syntax for this.

Trivially, since a filter takes a predicate that always is true or false, you would expect the return type to be boolean:

const sourceArr: number[] = [1, 2, 3, 4, 5, 6];
const isOdd = (item: number): boolean => item % 2 === 1;
console.log(sourceArr.filter(isOdd));

But in fact, you can specify what the type of item is when the predicate is true, by defining isOdd like a “type predicate”:

type Even = 2 | 4 | 6;
type Odd = 1 | 3 | 5; 
type OddOrEven = Odd | Even;

const sourceArr: OddOrEven[] = [1, 2, 3, 4, 5, 6];

const isOdd = (item: OddOrEven): item is Odd => item % 2 === 1;

console.log(sourceArr.filter<Odd>(isOdd));

Practical example: a response may have an error type

This example is contrived, so let me give a practical example with a fetch response that may be an error. Using the type guard guarantees that after the conditional the response is of one of the subtypes. You can also see that a type guard predicate can be used without using a filter.

Given the types and function:

interface SomeSuccess {
  data: string;
}

interface SomeError {
  message: string;
}

type SomeResponse = SomeSuccess | SomeError;

const mutation = (): SomeResponse => ({ data: 'some data' });

This would fail, because response could also be of type SomeSuccess:

const response = mutation();
console.error(response.message); // Error: Property 'message' does not exist on type 'SomeSuccess'

Checking with if(response.message) is not possible, for the same reason. You can check with the in operator:

const response = mutation();
if('message' in response) {
    console.error(response.message); // derived to be SomeError
    return;
}
console.debug(response.data); // derived to be SomeSuccess

But if you extract the conditional to a function, it would fail if it has a normal boolean return type:

const isError = (response: SomeResponse): boolean => 'message' in response;

const response = mutation();
if(isError(response)) {
    // Property 'message' does not exist on type 'SomeSuccess'
    console.error(response.message);
    return;
}
// Property 'data' does not exist on type 'SomeError'
console.debug(response.data);

And you can fix it by writing the conditional like a type guard:

const isError = (response: SomeResponse): response is SomeError => 'message' in response;

Practical example: undefined checks

Another useful application of a filter with a type guard is reducing checks for undefined values in your conditions. Assume you get an array of objects from a service, which we will call list. We want to get the property label for each of these objects and use these later. When only a map is used, the resulting array may contain undefined values:

interface Bat {
    id: number;
    label?: string;
}

const list: Array<Bat> = [{ id: 0, label: 'first' }, { id: 1 }];

const newList = list.map(n => n.label);

The type of newList is now derived as Array<string | undefined>. This makes it hard to follow up transformations, because you will have to perform undefined checks. We can fix this by doing this up front with a type guard we will call isDefined.

const isDefined = <T>(value: T | undefined): value is T => {
  return typeof value !== 'undefined';
};

const newList = list.map(n => n.label).filter(isDefined);

The type of newList is now derived as Array<string>. Note that for older versions of TypesScript if you would not use the type guard syntax value is T, this would not work and newList would still have the type Array<string | undefined>!

This has been fixed since TypeScript 5.5 with Inferred Type Predicates.

In the next chapter we will look at more functional operators, including Array.prototype.reduce.

Acknowledgement: this article was inspired by the course Functional-Light JavaScript, v3 by Kyle Simpson.