Instantly share code, notes, and snippets.

# briancavalier/typescript-strongly-typed-variadic-2.md Last active May 24, 2019

A technique for strongly-typed, heterogeneous variadic function types in Typescript

# Simpler Variadic Function Types in TypeScript (part 2): Higher-order Variadic Functions

In part 1 we looked at a technique that allows expressing strongly-typed variadic functions in a way that solves problems with the current most common approach. It reduces code size and repetition, and it scales to any arity.

I mentioned that the technique can also be extended to higher-order variadic functions, such as the typical `zipWith`. Let’s explore how to do that.

## ZipWith Example

As we did in part 1, let’s start with an example, a typical `zipWith` on arrays.

```function f (a: number, b: string, c: boolean): string {
return `\${a} \${b} \${c}`
}

const a: number[] = [1, 2, 3]
const b: string[] = ['foo', 'bar']
const c: boolean[] = [true, false]

const result = zipWith(f, a, b, c) // ’1 foo true’, ’2 bar false’```

In this case, we’d like the type of `result` to be inferred as `ReadonlyArray<string>`. That's straightforward. We can simply declare `f`’s return type using a type variable, e.g. `R`, and `zipWith`’s return type as `ReadonlyArray<R>`.

Notice, however, that `zipWith` is a higher-order variadic function. That is, it’s a variadic function whose first parameter is also a variadic function. Instead of mapping a tuple of arrays to an array of tuples, `zipWith` applies its variadic function parameter to each tuple, producing an array of the results.

Thus, instead of the relationship between `zipWith`’s parameter list type and return type, we need to consider the relationship between `zipWith`’s parameter list type and `f`’s parameter list type.

## Current Approach

The most common approach is the same one used for `zip`: enumerate explicit variations up to a point where you can’t imagine someone needing a higher arity.

```function zipWith<A, B, R>(f: (a: A, b: B) => R, a: ReadonlyArray<A>, b: ReadonlyArray<B>): ReadonlyArray<R>;
// ...
// ... and so on, argh!
// ...
// ... finally, we have to use any for the implementation, yuck!
// ...
}```

Clearly, this has the same issues as we saw with `zip`. In fact, it's slightly more verbose, since each variant needs to enumerate the types of two variadic parameter lists, rather than one and a tuple return type.

## Applying the Simpler Technique

To simplify the type of `zip`, we relied on TypeScripts parameter list tuple inference and used type-level programming to express the relationship between that inferred tuple type and the return type. However, in the case of `zipWith`, we need to express the relationship between two parameter lists.

There are two “directions” in which we can apply the technique:

1. by declaring `zipWith`’s parameter list type, and deriving `f`’s from it, or
2. by declaring `f`’s parameter list type, and deriving `zipWith`’s from it.

Let’s look at each.

## Declare zipWith, derive f

One important observation we can make about `zipWith` and `f` is that the `Zip` type we used in part 1 already expresses the mapping from `zipWith`’s parameter list to `f`’s. Here’s a quick reminder of the `Zip` type:

```// A type-level Zip, represented in TS as a mapped type, that maps a tuple of Arrays to the
// associated zipped type.
// For example, it maps types: [Array<number>, Array<string>] -> [number, string].
type Zip<A extends ReadonlyArray<any>> = {
[K in keyof A]: A[K] extends ReadonlyArray<infer T> ? T : never
}```

In the context of the `zipWith` example above, the `Zip` type maps `[Array<number>, Array<string>, Array<boolean>]` to `[number, string, boolean]`. Notice that `[Array<number>, Array<string>, Array<boolean>]` is the type of `zipWith`’s parameter list, and `[number, string, boolean]` is type of `f`’s parameter list. Thus, we can use `Zip` directly in the type of `zipWith`:

```function zipWith<Arrays extends ReadonlyArray<any>[], R>(f: (...args: Zip<Arrays>) => R, ...arrays: Arrays): ReadonlyArray<R> {
const len = Math.min(...arrays.map(a => a.length))
const zipped: R[] = new Array(len)
for (let i = 0; i < len; i++) {
// Next line is a type error!
// TypeScript can’t seem to resolve Zip<Arrays> and f’s parameter list type
zipped[i] = f(...arrays.map(a => a[i]) as Zip<Arrays>)
}
return zipped
}```

At first, it may seem surprising that we can reuse `Zip`. However, it makes perfect sense given that `f` effectively is receiving the output (albeit incrementally) of the first-order `zip`, whose return type was derived using `Zip`.

Unfortunately, TypeScript can’t seem to resolve the `Zip<Arrays>` type with the type of `f`’s parameter list in the `zipWith` implementation. Here’s a playground showing the type error. My intuition is that it has something to do with TypeScript’s handling of parameter variance, but I’ve not investigated it further. If you have any insight, I’d love to hear from you!

Fortunately, there is a solution. Introducing a type parameter for `f`’s parameter list seems to help TypeScript unify the types, and we can get to a working version:

```// Note the additional Args type parameter
function zipWith<Arrays extends ReadonlyArray<any>[], Args extends Zip<Arrays>, R>(f: (...args: Args) => R, ...arrays: Arrays): ReadonlyArray<R> {
const len = Math.min(...arrays.map(a => a.length))
const zipped: R[] = new Array(len)
for (let i = 0; i < len; i++) {
// Typescript needs the Args hint or it infers any[]
zipped[i] = f(...arrays.map(a => a[i]) as Args)
}
return zipped
}```

Here’s a playground of the working version. It’s enlightening to hover over the final `zipWith` call to see the actual inferred type of the call site. I’ve had issues with TypeScript’s inference in the past, but I have to give kudos in this instance!

## Declare f, derive zipWith

Now we know how to make the types work in one “direction”, but we found that we had to help TypeScript with an “extra” type parameter. Let’s try the other direction to see how it compares.

In this case, we’ll declare `f`’s parameter list type, and derive the type of the tail of `zipWith`’s parameter list from it. Since we’re working in the opposite direction, we need to invert the direction of the `Zip` type. Instead of a type-level function that maps `[Array<number>, Array<string>] -> [number, string]`, i.e. from `zipWith`’s parameter list type to `f`’s, we need one that performs the reverse mapping, `[number, string] -> [Array<number>, Array<string>]`, i.e. from `f`’s parameter list type to `zipWith`’s. Let’s call it `UnZip`.

```// A type-level UnZip.
// For example, it maps types: [number, string] -> [Array<number>, Array<string>].
type UnZip<A extends ReadonlyArray<any>> = {
}```

We can use `UnZip` to write the type of `zipWith`:

```function zipWith<Args extends any[], R>(f: (...args: Args) => R, ...arrays: UnZip<Args>): ReadonlyArray<R> {
const len = Math.min(...arrays.map(a => a.length))
const zipped: R[] = new Array(len)
for (let i = 0; i < len; i++) {
// Typescript needs the Args hint or it infers any[]
zipped[i] = f(...arrays.map(a => a[i]) as Args)
}
return zipped
}```

Here’s a playground of this version. Once again, TypeScript does quite an amazing job inferring the type of the final call site. It’s a pleasant and unexpected (for me, anyway) bonus that TypeScript infers `readonly` for each of the arrays.

## Which one to use?

Use the one that fits your needs the best.

If the first didn't require the "extra" type parameter, I think I would prefer it from an expressiveness perspective. I feel it expresses the intent more clearly by using the same `Zip` type to show that `f` receives zipped tuples. However, the inferred `readonly` of the second is attractive. Perhaps there is another solution that has both the expressiveness of the first, and the `readonly` of the second. Until then, pick what's most important to you and go with it.

## Generalizing further

Part 1 mentioned how the technique can be generalized to any variadic function whose return type depends on its parameter list. In part 2, we’ve seen that it can also be applied to the relationship between any two variadic parameter lists.

The idea is even more general, and I’ll bet it isn’t new--although I believe its application to variadic functions in TypeScript may be new. It can be applied to express the relationship between any two types, and is a good fit when expressing the types directly doesn’t scale well:

Derive one type from the other using a type-level function.