Boost Your App's Performance by Wrapping Your Functions Inside a Pipe
Tags: angular, typescript
Published: 2023-04-07 07:50:00

The purpose of this article is to provide you with a small, strongly-typed utility pipe that you can easily incorporate into your project to improve its performance.

This article will also provide the solution for [challenge #9](https://github.com/tomalaforge/angular-challenges/blob/main/apps/pipe-intermediate/README.md) of Angular Challenges, which is intended for intermediate-level developers seeking to enhance their understanding of pipes. If you haven’t tried it yet, I encourage you to do so before coming back to compare your solution with mine. _(You can also submit a PR that I’ll review)_

---

In this article, we will not delve into the workings of pipes or how they can benefit your Angular application. If you wish to learn more about these topics, please refer to my previous article.

{% embed https://dev.to/this-is-angular/deep-dive-into-angular-pipes-53dl %}

As mentioned in the previous article, calling a function inside a template can have a significant impact on performance as the function will be recomputed every time change detection is executed. We can mitigate this by adding a `memo` function to cache the input value, as described in the above article. However, this approach has a limitation in that we can only use the memo function once per input.

The solution to this problem is to move our function inside a pipe. However, if we have multiple functions to move, we would need to create a new pipe for each function, which could become cumbersome.

In challenge #9 of Angular Challenge, we have the following component:

```typescript
@Component({
  standalone: true,
  imports: [NgFor],
  selector: 'app-root',
  template: `
    <div *ngFor="let person of persons; let index = index; let isFirst = first">
      {{ showName(person.name, index) }}
      {{ isAllowed(person.age, isFirst) }}
    </div>
  `,
})
export class AppComponent {
  persons = [
    { name: 'Toto', age: 10 },
    { name: 'Jack', age: 15 },
    { name: 'John', age: 30 },
  ];

  showName(name: string, index: number) {
    // very heavy computation
    return `${name} - ${index}`;
  }

  isAllowed(age: number, isFirst: boolean) {
    if (isFirst) {
      return 'always allowed';
    } else {
      return age > 25 ? 'allowed' : 'declined';
    }
  }
}
```
The for loop in our code is currently calling two functions for each person in the array. These functions are being recomputed at each change detection cycle, which can cause performance issues.

As mentioned previously, we could create a separate `ShowNamePipe` and `IsAllowedPipe` for each function, but this would require creating a large number of pipes for every component in our application.

To solve this problem, we can wrap our function inside a generic pipe called `WrapFnPipe`, as shown below:

```ts
@Pipe({
  name: 'wrapFn',
  standalone: true,
})
export class WrapFnPipe implements PipeTransform {
  transform(func: (...arg: any[]) => R, ...args: any[]): R {
    return func(...args);
  }
}
```
To refactor our HTML with this pipe, the first argument is the function we want to wrap, then we pass all the function arguments separated by `:`. We can now rewrite our HTML to the following:

```html
<div *ngFor="let person of persons; let index = index; let isFirst = first">
  {{ showName | wrapFn : person.name : index }}
  {{ isAllowed | wrapFn : person.age : isFirst }}
</div>
```
By using this method, we can obtain all the benefits of Angular pipe without creating a specific pipe for each function. Each function is wrapped inside its own instance of the `WrapFnPipe`, which means each person has its own cached value. Therefore, when a new change detection is executed, the function is not re-executed and the last cached value is returned, resulting in an immediate performance boost.

However, introducing this wrapping function removes all type safety as it remaps all types to `any`. To add type safety, let’s explore some options.

A naive approach to adding type safety would be to use generics and replace all `any` types with a generic type:

```ts
transform<ARG, R>(func: (...arg: ARG[]) => R, ...args: ARG[]): R {
  return func(...args);
}
```
However, this approach will only work if all arguments of the function are of the same type.

![codee block error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lbw7rhyvopycccpx0pot.png)


In our case, we will encounter an error because the generic type `ARG` only takes the first type, which is `string` in our case, and not `string | undefined`. This approach would not be very type safe either because we would be able to invert both arguments.

To address this issue, we need to explore TypeScript’s function overloading feature and take advantage of it. If you are not familiar with function overloading or how it works, you should read the following article first and come back to this article afterwards.

{% embed https://dev.to/this-is-angular/function-overloading-in-typescript-53eb %}

Using function overloading, we end up with this pipe `transform` function:

```ts
@Pipe({
  name: 'wrapFn',
  standalone: true,
})
export class WrapFnPipe implements PipeTransform {
  transform<ARG, R>(func: (arg: ARG) => R, args: ARG): R;
  transform<ARG1, ARG2, R>(
    func: (arg1: ARG1, arg2: ARG2) => R,
    arg1: ARG1,
    arg2: ARG2
  ): R;
  transform<ARG1, ARG2, ARG3, R>(
    func: (arg1: ARG1, arg2: ARG2, arg3: ARG3) => R,
    arg1: ARG1,
    arg2: ARG2,
    arg3: ARG3
  ): R;
  transform<ARG1, ARG2, ARG3, R>(
    func: (arg1: ARG1, arg2: ARG2, arg3: ARG3, ...arg: any[]) => R,
    arg1: ARG1,
    arg2: ARG2,
    arg3: ARG3,
    ...arg: any[]
  ): R;

  transform<R>(func: (...arg: unknown[]) => R, ...args: unknown[]): R {
    return func(...args);
  }
}
```
**Note:**

- The first 4 functions are just definitions for different `transform` methods. The last one is the actual implementation that combines all of the definitions.
- The first definition is for a one argument function, the second for a two arguments function and so on. We stop at 4 because it is bad design to have too many arguments, but if you want to, more definitions can be added.

By doing so, we have strong type safety in our template for any function. This includes type safety on the number of parameters, as well as type safety on the type of each input, as shown in the following screenshot:


![the number of argument is wrong](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/toihp1hpv0xl7aprnvek.png)


![The type of the second argument is incorrect](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxi54bjvk22rfqh3zzhz.png)

---

I hope this article has helped you understand the power of pipes and what can be achieved with TypeScript. This pipe is a quick win and can be added to your project right away to boost some very costly operations without the need to refactor everything. 🚀

You can find me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge).