Use Typescript mapped types to "Promisify" interfaces
Tags: typescript
Published: 2021-04-13 12:07:30

## Intro
Imagine you're specifying an interface in Typescript with a bunch of methods (sync and async) and attributes, randomly, this one:

```ts
// This is the "Original interface" we'll refer to during this article.
interface DataService {
   cache: Map<string, Data>;
   invalidateCache();
   processData( data: Data ): Promise<Result>;
   compressData( data: Data ): Result;
}
```  

Now imagine again that you need to derive it to another interface that will only expose it's **methods**: 

```ts
interface DataService {
   invalidateCache(): void;
   processData( data: Data ): Promise<Result>;
   compressData( data: Data ): Result;
}
```  

Or to another one that will **convert all sync method to async**:

```ts
interface DataService {
   invalidateCache(): Promise<void>;
   processData( data: Data ): Promise<Result>;
   compressData( data: Data ): Promise<Result>;
}
```  

Let's see how we can do that using Typescript [Mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html)

# Filter out attributes from an interface:

First thing we want to do is to convert the "DataService" interface to this one:

```ts
interface DataServiceWithoutAttributes {
   invalidateCache();
   processData( data: Data ): Promise<Result>;
   compressData( data: Data ): Result;
}
```  

As you can see the only thing that changes between the original version and the "remapped one" is that the `cache` attribute is gone.

Here is the `Mapped type` used to do that: 

```ts
type FilterOutAttributes<Base> = {
  [Key in keyof Base]: Base[Key] extends (...any) => any ? Base[Key] : never;
}
```

And here is how to use it:

```ts
type DataServiceWithoutAttributes = FilterOutAttributes<DataService>;
```

Let's break this down:

```ts
type FilterOutAttributes<Base>
```
This declares a mapped type named `FilterOutAttributes` that takes one generic parameter `Base`. 
We'll use it to provide the "original" interface that we want to remap.

```ts
{
  [Key in keyof Base]: ...
}
```

The `keyof` operator will return a list of all the keys available in Base and the `in` operator will iterate over that list and store each value in `Key`
This is like writing the following loop:
```js
const Keys = ["cache", "invalidateCache", "processData", "compressData"];
for( let i=0; i<Keys.length; i++ ) {
  const Key = Keys[i];
}
```

Now, for each key, we need to determine if it will be present in the mapped type or not. (or in our case, if the key matches a function in the Base interface)

To do so we need a way to express condition:
```js
if( Base[Key] is a function ) {
  It's a function
} else {
  It's not a function
}
```
Or using ternary condition:
```
Base[Key] is a function ? It's a function : It's not a function;
```

Well, turns out we can express that condition using [Conditional types] in typescript:(https://www.typescriptlang.org/docs/handbook/2/conditional-types.html)

```ts
Base[Key] extends (...any) => any ? ... : ...
```

See! This is a ternary expression and the left side is the test expression:
If `Base[Key]` extends any kind of function ( `(...any) => any` ) then the condition is true.

> Thinks of extends as a mechanism to constraint the parameter type to match what we want, you can mentally replace it by "can be safely converted into". So in our case, we could read that as: If `Base[Key]` can be safely converted into `(...any) => any` then expression is true

So if the condition is `true`, we return `Base[Key]` from the ternary expression, which corresponds to the type of the attribute `Key` in the `Base`object.

And if the condition is `false`, we return the [keyword](https://www.typescriptlang.org/docs/handbook/basic-types.html#never) `never` which will discard the current `Key` from the mapped type.

# Convert sync methods to async methods:

What we want to do is to convert the "DataServiceWithoutAttributes" interface to this one:

```ts
interface DataServicePromisified {
   invalidateCache(): Promise<void>;
   processData( data: Data ): Promise<Result>;
   compressData( data: Data ): Promise<Result>;
}
```  

Now, each method returns a "Promisified" version of the original: 
The return value is kept but wrapped inside a promise.

Before going into the detail of this mapped type, let's introduce two builtin mapped type provided by Typescript:
- `ReturnType<Base>` If Base is a function, this will return the type of the return value of that function
- `Parameters<Base>` If Base is a function, this will return the type of every parameter of that function as a tuple

Using those 2 primitives, we'll build a third one: `PromisifyFunction<Base>`.
This mapped type will convert a sync function into an async one by wrapping its return value inside a Promise.

```ts
type PromisifyFunction<Function extends (...any) => any> =
  (...args: Parameters<Function>) => Promise<ReturnType<Function>>;
```

So: 
- If the `Function` parameter is a function: `extends (...any) => any`
- Return a function that:
 - take the same set of parameters: `(...args: Parameters<Function>)`
 - Wrap the return type inside a promise: `Promise<ReturnType<Function>>`

Ok, so now that have our 3 primitives let's have a look at the implementation of the `PromisifyObject<Base>` mapped type:

```ts
type PromisifyObject<Base extends { [key: string]: (...any) => any }> = {
  [Key in keyof Base]: ReturnType<Base[Key]> extends Promise<any> ?
  Base[Key] :
  PromisifyFunction<Base[Key]>;
}
```

Now, this should make sense for you as we are following the same pattern as in the previous chapter:
- We remap each key of the input parameter: `[Key in keyof Base]`
- For each key we use a ternary operator to decide what to do: `ReturnType<Base[Key]> extends Promise<any> ?
  ... : ...` 
- If the return value of the current key in the input parameter is a Promise: `ReturnType<Base[Key]> extends Promise<any>`
- Then we return that the same type
- Otherwise use the `PromisifyFunction<Base>` defined earlier to convert this sync function into an async one.

And here we are!
Now we can combine our two custom mapped type to "Promisify" any kind of interface:

```ts
type DataServicePromisified = PromisifyObject<FilterOutAttributes<DataService>>; 
```

The syntax is not the easiest to read, but once you get the things with the extends mechanism to apply constraint, and how to use it within a ternary operator, understanding such code is much easier :) 

Now you may ask:
Okay, that's funny, but at which point in my day-to-day life is this thing useful?

Weel, check out [my previous article](https://dev.to/clementvidal/use-es6-proxy-for-isolated-context-intercommunication-mc1) for an example of where that could help you!

I hope you enjoy reading this article and that it was useful for you ;) 

Happy (typed) coding!