Forever Functional: Complex Typing in TypeScript
Published: 2023-06-23 02:26:41

by [Federico Kereki](https://blog.openreplay.com/authors/federico-kereki)

<blockquote><em>Data typing is usually simple, but with functional techniques, recursion, and more, it can become complex. This article will complement the previous one on partial application, by developing full typing in TypeScript.</em></blockquote>

In the [previous article](https://blog.openreplay.com/forever-functional-working-with-functions-partially/) in this series, we learned how to do partial application in JavaScript, and developed a higher-order `partial()` function... but what about a TypeScript version?

It happens that typing our function is not trivial, and requires several interesting techniques, so in this article we'll complete the work from the previous article, going from JavaScript to TypeScript, and adding full type controls. (For more detail, you can check out my [Mastering JavaScript Functional Programming](https://www.amazon.com/Mastering-JavaScript-Functional-Programming-maintainable/dp/1804610135/ref=sr_1_5) book, in which I also discuss currying and other transformations.) You may want to look at our code again, to have it fresh in mind.

## Check matching types

When analyzing the provided and missing parameters, we must check that types agree among them, one by one. However, when defining types we cannot just write an `if` or a loop, so we'll have to find a workaround: our declaration will use ternary operators and recursion in place of `if`s and loops. 

We will write a `TypesMatch<P,A>` type declaration (`P` stands for "parameters", and `A` for "arguments"; both will be tuples with types) that will either produce `boolean` (if `P` and `A` matched) or `never` (if there was a mismatch). The declaration is as follows:

```typescript
type TypesMatch<
  P extends any[],
  A extends any[]
> = 0 extends P["length"]                      
  ? boolean                      1️⃣
  : 0 extends A["length"]        
  ? boolean                      2️⃣
  : [P, A] extends [         
      [infer PH, ...infer PT],
      [infer AH, ...infer AT]?
    ]
  ? AH extends undefined       
    ? TypesMatch<PT, AT>         3️⃣
    : PH extends AH             
    ? TypesMatch<PT, AT>         4️⃣
    : never
  : never;
```

The `0 extends P["length"]` line 1️⃣ may confuse you, but it's the way of checking if the length of `P` is zero. We use `infer` and spreading to separate the first types of `P` and `A` (`PH` and `AH`; `H` is for "head") from the rest (`PT` and `AT`; `T` is for "tail").

Our type is as follows:

* if either `P` 1️⃣ or `A` 2️⃣ is empty, return `boolean`
* if the first type in `A` is undefined 3️⃣ or if the first type in `A` matches the first type in `P` 4️⃣ discard the first type in `P`, discard the first type in `A`, and recursively analyze the remaining types
* otherwise (if the first type in `A` is not undefined, but doesn't match the first type in `P`) return `never`

We can do some quick tests to verify everything is in order -- the "ok" types are `boolean` ( meaning they are correct) and the "bad" types are `never` (so they are wrong):

```typescript
type ok1 = TypesMatch<
  [boolean, number, string],
  [undefined, undefined, undefined]
>;
type ok2 = TypesMatch<
  [boolean, number | string, string],
  [boolean, undefined, string]
>;
type ok3 = TypesMatch<
  [boolean, number | string, string],
  [boolean, string, string]
>;

type bad1 = TypesMatch<
  [boolean, number, string],
  [undefined, string, number]
>;
type bad2 = TypesMatch<
  [boolean, number | string, string],
  [string, undefined, string]
>;
type bad3 = TypesMatch<
  [boolean, number | string, string],
  [boolean, boolean, string]
>;
```

## Deduce pending parameters

We'll need another auxiliary type, `Partialize<P,A>`, that will receive a tuple with the parameter types and another with the argument types, and produce a tuple with the types for the still not provided arguments; i.e., those types in `P` for which there's a corresponding `undefined` type in `A`. Let's assume we already checked that types match (as we saw in the previous section). We can write types as follows.

```typescript
type Partialize<
  P extends any[],
  A extends any[]
> = 0 extends P["length"]                 
  ? []                                1️⃣
  : 0 extends A["length"]                 
  ? P                                 2️⃣
  : [P, A] extends [
      [infer PH, ...infer PT],
      [infer AH, ...infer AT]
    ]
  ? AH extends undefined                 
    ? [PH, ...Partialize<PT, AT>]     3️⃣
    : [...Partialize<PT, AT>]         4️⃣
  : never;
```

The type definition is very similar in style to `TypesMatch`, though the results vary. 

* 1️⃣ if `P` is empty (all parameters were provided) the result is empty as well
* 2️⃣ if `A` is empty (no arguments, all parameters are pending), the result is `P`
* 3️⃣ if the first type in `A` is undefined, the result will be the first type in `P` (because it wasn´t provided) followed by the result of processing the rest of the types in `P` and `A`.
* 4️⃣ otherwise, if the first type in `P` matches the first type in `A`, the result will be whatever is returned by comparing the rest of the types in `P` to the rest of the types in `A`

We can check how `Partialize` works:

```typescript
type part1 = Partialize<
  [boolean, number, string],
  [undefined, undefined, undefined]
>; // boolean,number,string
type part2 = Partialize<
  [boolean | string, number, string],
  [undefined, number, undefined]
>; // boolean|string,string
type part3 = Partialize<
  [boolean | string, number, string],
  [boolean, undefined, string]
>; // number
type part4 = Partialize<
  [boolean, number, string],
  [boolean, number, string]
>; // empty!
```


<div style="background-color:#efefef; border-radius:8px; padding:10px; display:block;">
<hr/>
<h3><em>Session Replay for Developers</em></h3> 
<p><em>Uncover frustrations, understand bugs and fix slowdowns like never before with <strong><a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a></strong> — an open-source session replay suite for developers. It can be <strong>self-hosted</strong> in minutes, giving you complete control over your customer data.</em></p>
<img alt="OpenReplay" style="margin-top:5px; margin-bottom:5px;" width="768" height="400" src="https://raw.githubusercontent.com/openreplay/openreplay/main/static/openreplay-git-hero.svg" class="astro-UXNKDZ4E" loading="lazy" decoding="async">
<p><em>Happy debugging! <a href="https://openreplay.com" target="_blank">Try using OpenReplay today.</a></em><p>
<hr/>
</div> 



## Complete typing 

Now that we have a way to check that the parameters and arguments match, and also how to calculate the still pending parameters, we can actually write our `Partial` type declaration.

In the definition below, `P` will represent the types of the function's parameters, `A` the types of the provided arguments, and `R` the type of the function's result.

```typescript
type Partial<P extends any[], R> = <A extends any[]>(
  ...x: A
) => TypesMatch<P, A> extends never
  ? never
  : P extends any[]
  ? 0 extends Partialize<P, A>["length"]
    ? R
    : Partial<Partialize<P, A>, R>
  : never;
```

How does this work?
* if the types in `P` and `A` do not match, we return a `never` result, which means there was a problem
* if `A` is empty (no arguments provided) then our partialized function will take parameters of type `P` and produce a result of type `R`
* if `A` is not empty, then we compute the result of comparing types `P` and `A`, and that will be the types of the parameters to our partialized function, that will produce a result of type `R`

This data type definition would be very hard to understand on its own, but since we already saw all its components, it's understandable. 

## Finish the job

OK, it seems we're ready to finish now! Here's the full definition of `partial(...)` in TypeScript -- and we'll have to deal with another typing detail! The issue will be that TypeScript, though usually being able to deduce types on its own, isn't able to work out how our function works and what it returns, so we'll have to do a trick to help it.

```typescript
function partial<P extends any[], R>(              1️⃣
  fn: (...a: P) => R
): Partial<P, R>;                                  
function partial(fn: (...a: any) => any) {         2️⃣ 
  const partialize =
    (...args1: any[]) =>
    (...args2: any[]) => {
      for (
        let i = 0;
        i < args1.length && args2.length;
        i++
      ) {
        if (args1[i] === undefined) {
          args1[i] = args2.shift();
        }
      }
      const allParams = [...args1, ...args2];
      return allParams.includes(undefined) ||
        allParams.length < fn.length
        ? partialize(...allParams)
        : fn(...allParams);
    };
  return partialize();
}
```

We are defining the type of `partial()` twice: once using our typing 1️⃣ and the second time 2️⃣ with generic `any` values. The issue is that TypeScript cannot "understand" the function enough to work out the result, so we "overload" the type definition, and then it's our responsibility to ensure data types are correct!

Let's finish with a few examples of fully typed partial application at work.

```typescript
const nonsense = partial(function (
  a: number,
  b: string,
  c: boolean
) {
  return `${a}/${b}/${c}`;
});

const ns1 = nonsense(undefined, "9", undefined); 
// type: Partial<[number, boolean], string>

const ns2 = nonsense(22, "9", undefined); 
// type: Partial<[boolean], string>

const ns3 = nonsense(22, "9", true); 
// type: string -- its value is "22/9/true"

const ns4 = nonsense(undefined,"X",undefined)(undefined,false);
// type: Partial<[number], string>
```

We can check types:
* `ns1` fixed the 2nd parameter of the `nonsense` function, so we now have a `[number,boolean]` to `string` function.
* `ns2` fixed two parameters, so the result is a `[boolean]` to `string`
* `ns3` fixes all parameters, so the result is a plain `string`
* `ns4` fixes parameters in two steps; the result is a `[number]` to `string` function

## Conclusion

This was an example of complex typing; we needed to use recursion instead of loops, ternary operators instead of alternative structures, and produce a type instead of returning a value. We also saw that, even with all this work, TypeScript can have problems determining types, but there is a way out.