Extending the Array map() function
Tags: typescript
Published: 2020-02-28 19:43:10

---
title: Extending the Array map() function
published: true
description: Adding functions to the array type to allow for more concise syntax
tags: TypeScript
cover_image: https://thepracticaldev.s3.amazonaws.com/i/gcsylup57n3n2sa8ckuf.jpg
series: typescript
---

When writing TypeScript or JavaScript code, you'll come across a lot of code snippets like this:

```typescript
const result_1 = myArray.map(x => doSomething(x));
const result_2 = myArray.map(x => x.property);
const result_3 = myArray.map(x => x.toString());
```

These lines of code are pretty similar - they produce an array from an input array. This is done by doing one of a few things on each element of the input array: calling a function with the element as the argument, accessing a property on the element, or calling one of the element's class methods. With [a few funny exceptions](https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap), calling a function with the argument being the array element can be abbreviated like this:

```typescript
const result_1 = myArray.map(doSomething);
```

If the function `doSomething` only takes one argument and does not reference a `this` object we would usually by fine using this shorter syntax. The other two examples above can't be abbreviated like that, and I'll show a [subjectively] elegant solution here.

## Overloading Array<T>.map()

### Property Access

I want to be able to do the property access without that `x => x.<prop>` syntax. I want to achieve this:

```typescript
const result_2 = myArray.map('property');
const result_2 = myArray.map('undefinedProperty'); // error here
```

The function and its type signature were actually the simpler part for me to implement, it's adding to the basic `Array<T>` type that took some trials to get to, but here's the result:

```typescript
interface Array<T> {
    mapTo<K extends keyof T>(k: K): Array<T[K]>;
}

Array.prototype.mapTo = function <T, K extends keyof T>(this: T[], k: K) {
    return this.map(x => x[k]);
};
```

The use of the `keyof` keyword ensures that for an array with elements of type `T` we can only provide the name of one of `T`'s properties. The `x => x[k]` syntax is used in the implementation, but we don't have to use it ever again. We can map as we always have, or use this overload in case we're only doing a property access.

### Function Call

This one is slightly trickier, since the generic type parameter `T` isn't necessarily a function type. We want to be able to map from an array of functions to an array of results of those functions being called, but we can't simply go `x => x(...args)` for any old `x` of type `T`. To accomplish this, we'll introduce our own subtype of `Array`, the `FunctionalArray` (that is not to say ordinary arrays are dysfunctional, though ;)

```typescript
class FunctionalArray<A extends any[], R> extends Array<(...args: A) => R> {
    mapToCall(...args: A): R[] {
        return this.map(x => x(...args));
        // consider the alternative:
        return this.map(function(x) { return x(...args); })
    }
}
```

A `FunctionalArray` is an `Array` where each element is a function, and has two type parameters - `A` for the function's argument types, and `R` for its return type. This lets us easily define `mapToCall` using those type parameters, and we're free to use the array elements as functions because we've defined `this` to be an array of functions of the appropriate types.

The next step is to use our new `FunctionalArray` class with our new `Array<T>.mapTo` so we can actually do something cool like this:

```typescript
[1, 2, 3].map(x => x.toExponential()); // rewrite as:

[1, 2, 3].mapTo('toExponential').mapToCall();
```

To do this we can have `mapTo` return a `FunctionalArray` if it can:

```typescript
interface Array<T> {
    mapTo<K extends keyof T>(k: K): T[K] extends (...args: infer A) => infer R ? FunctionalArray<A, R> : Array<T[K]>;
}
```

This return type means that for an array of type `T` where type `T[K]` is a function, `mapTo` will actually return the correct `FunctionalArray` with the relevant type parameters, or just an `Array<T[K]>` otherwise.

We can now replace mapping an array using a simple property access with `mapTo` and calling a method with `mapTo` followed by a `mapToCall`. An contrived example using an object literal with some method:

```typescript
[{ omg(x?: number): number { return 2; } }].mapTo('omg').mapToCall(2); // optional argument can be provided
[{ omg(x?: number): number { return 2; } }].mapTo('omg').mapToCall(); // or omitted
[{ omg(x: number): number { return 2; } }].mapTo('omg').mapToCall(); // an error is produced if a required argument is not provided
```

### Partial Function Application

I can imagine the function being called with `mapToCall` might require several arguments, which might not all be available at the location where the mapping is invoked. In this case we might consider providing some of the arguments and providing a `FunctionalArray` of [curried functions](https://dev.to/bettercodingacademy/functional-programming-all-you-need-to-know-about-currying-5k7). Consider this code:

```typescript
type someFunctionType = (a: string, b: number, c: string, d: number) => string;

let func: someFunctionType;

// expected type: FunctionalArray<[string, number], string>
const curriedResult = ([func] as FunctionalArray<[string, number, string, number], string>).mapToCall("hello", 1);
```

In the above example, `mapToCall` was called with only the first two arguments, and returned a `FunctionalArray` of partially-applied functions - when called, the functions stored in `curriedResult` would call `func` with the first two arguments being fixed as `'hello'` and `1`.

There's [A great StackOverflow answer on typing curried functions](https://stackoverflow.com/a/51860718/6338059) by SO user jcalz, so it seems possible to type this overload of `mapToCall` correctly, even if it does require manually unrolling type signatures for various numbers of function arguments. I think it goes a bit beyond the scope of this post, but I might write about it in the future.