Limit Typescript decorator usage to functions that return either void or Promise<void>

I've been working on developing a decorator that is specifically designed for methods of type void or Promise<void>.

class TestClass {
  // compiles successfully
  @Example()
  test() {}

  // should compile, but doesn't
  @Example()
  async testPromise() {}

  // fails to compile as expected
  @Example()
  async testBad() {
    return 'test';
  }

  // fails to compile as expected
  @Example()
  async testBadPromise() {
    return 'test';
  }
}

While I understand the approach to refine decorated types in general, I am struggling to create a union type that includes promises. The minimal example below works for void methods, but not for Promise<void>:

type VoidFn = ((...args: any[]) => void) | ((...args: any[]) => Promise<void>);

const Example = () => (
  target: any,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<VoidFn>,
) => {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const result = originalMethod!.apply(this, args);
    return result;
  };
  return descriptor;
};

Additional points if it can capture methods that might potentially return undefined, such as

Promise<string | undefined>
. My specific goal is to catch, log, and handle errors without disrupting the method's flow. Ultimately, the desired functionality looks like this:

const ErrorCapture = () => (
  target: any,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<VoidFn>,
) => {
  const originalMethod = descriptor.value;
  if (originalMethod) {
    descriptor.value = function(this: any, ...args: any[]) {
      const handleError = (error: any) => {
        console.error(error);
        // etc.
      };
      try {
        const result = originalMethod.apply(this, args);
        if (result instanceof Promise) {
          return result.catch(handleError);
        }
      } catch (error) {
        handleError(error);
      }
    };
    return descriptor;
  }
};

Answer №1

I successfully got your sample to function as desired. The key here is leveraging function overloads, which introduces additional signatures to a function. Here's the finalized code:

const Example = () => {
  function dec(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise<void>>
  ): TypedPropertyDescriptor<(...args: any[]) => Promise<void>;
  function dec(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => void>
  ): TypedPropertyDescriptor<(...args: any[]) => void>;
  function dec(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const result = originalMethod!.apply(this, args);
      return result;
    };
    return descriptor;
  }
  return dec;
};

class TestClass {
  // compiles correctly
  @Example()
  test() {}

  // compiles without issue
  @Example()
  async testPromise() {}

  // fails compilation as expected
  @Example()
  testBad() {
    return 'test';
  }

  // fails compilation as expected
  @Example()
  async testBadPromise() {
    return 'test';
  }
}

I have also created a TypeScript Playground.

This functionality operates by having the Example function produce another function with multiple overloads for each applicable method. However, note that the third signature pertains to the implementation details.

Answer №2

I would like to express my gratitude to @blaumeise20 for guiding me in the right direction. Finally, I was able to figure out the types using the new overload syntax that allows it to be applied to functions that might return null, whether synchronous or asynchronous:

type Fn<T> = (...args: any[]) => T;

type CaptureType = {
  <T>(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<Fn<T | null>>,
  ): TypedPropertyDescriptor<Fn<T | null>>;
  <T>(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<Fn<Promise<T> | Promise<null>>>,
  ): TypedPropertyDescriptor<Fn<Promise<T> | Promise<null>>>;
  (
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<Fn<Promise<void>>>,
  ): TypedPropertyDescriptor<Fn<Promise<void>>>;
};

/**
 * Handles and reports any errors that arise during the function's execution
 * while also suppressing the error. Since there is no return value when an
 * error is suppressed, this functionality is only applicable to methods of type `void`
 * or those that can return either `null` or `Promise<null>`.
 */
function CaptureErrors(): CaptureType {
  return (
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
  ) => {
    const originalMethod = descriptor.value;
    if (originalMethod) {
      descriptor.value = function (this: any, ...args: any[]) {
        const onError = (error: any) => {
          console.error(error);
          return null;
        };

        try {
          const result = originalMethod.apply(this, args);
          if (result && typeof result.then === 'function') {
            return result.then(undefined, onError);
          } else {
            return result;
          }
        } catch (error) {
          return onError(error);
        }
      };
    }

    return descriptor;
  };
}
class TestClass {
  @CaptureErrors()
  voidTest() {
    causeError();
  }

  @CaptureErrors()
  async asyncVoidTest() {
    causeError();
  }

  @CaptureErrors()
  nullableTest(): string | null {
    causeError();
    return 'foo';
  }

  @CaptureErrors()
  async asyncNullableTest(): Promise<string | null> {
    causeError();
    return 'foo';
  }

  // @ts-expect-error won't compile because not nullable
  @CaptureErrors()
  nonNullableTest() {
    return 'foo';
  }

  // @ts-expect-error won't compile because not nullable
  @CaptureErrors()
  async asyncNonNullableTest() {
    return 'foo';
  }
}

Typescript Playground

Answer №3

I attempted to mimic your code and discovered some intriguing outcomes.

Case 1: applying a decorator to a regular function TS Playground

class TestClass {
  // compiles correctly
  @Example()
  test() {}

  @Example()
  testString(): VoidReturned { 
    return 'test';
  }
}

type VoidReturned = void | string;
type VoidFn<T> = (...args: any[]) => T;

const Example = () => (
  target: any,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<VoidFn<VoidReturned>>,
) => {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const result = originalMethod!.apply(this, args);
    return result;
  };
  return descriptor;
};

In Case 1, TypeScript infers that testString() is of type () => string, which is incorrect as the descriptor expects a function of type

() => void | string</code. To rectify this, we must add <code>: VoidReturned
so that the type of testString() becomes () => void | string

Case 2: applying a decorator to an async function TS Playground

class TestClass {
  // now functioning
  

Similar to Case 1, we need to include : Promise<VoidReturned> to ensure the types align. When utilized separately, they operate as expected.

Case 3: using a decorator on both regular and async functions TS Playground: Note that normal and async functions differ only in their return type. We have created a new type called MixedFn to encompass both normal and async functions. Each method must have a return type that matches the return type of MixedFn

class TestClass {
  // correctly compiles
 

There is only one issue remaining: according to JavaScript specifications, async functions must only have a return type of Promise<T>. However, our async methods currently have a type of

VoidReturned | Promise<VoidReturned></code. To resolve this, we can convert async functions into normal functions that return a Promise instead:</p>
<pre><code>class TestClass {
  

Final code here; I also modified the implementation of Example to reference this as the context for the original method

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular 6 - The requested resource does not have the necessary 'Access-Control-Allow-Origin' header

I am currently working on an Angular 6 project that includes a service pointing to server.js. Angular is running on port: 4200 and Server.js is running on port: 3000. However, when I try to access the service at http://localhost:3000/api/posts (the locat ...

The best approach for sending parameters to the parent class in TypeScript for optimal efficiency

What's the optimal solution to this problem? I really appreciate how we can specify attributes in the constructor and TypeScript takes care of handling everything to assign values to the props in JavaScript - like I did with 'department' her ...

Error in Typescript: Cannot assign type 'string[]' to type 'string'

I'm currently developing a project using Next.js with Typescript and .tsx files, and I'm utilizing Supabase as my database. While everything is functioning perfectly on localhost, I'm encountering an issue when trying to build the project o ...

What is the process of creating a for loop in FindById and then sending a response with Mongoose?

Is there a way to get all the data in one go after the for loop is completed and store it in the database? The code currently receives user object id values through req.body. If the server receives 3 id values, it should respond with 3 sets of data to th ...

I am currently working on an Angular 8 project and experiencing difficulties with displaying a specific value from a JSON object in my template using an ngFor loop

Apologies if I am not familiar with all the terms, as I am mostly self-taught. I started with Udemy and then turned to Stack Overflow to tackle the more challenging aspects. This platform has been incredibly valuable and I am truly grateful for it. Now, l ...

Developing maintenance logic in Angular to control subsequent API requests

In our Angular 9 application, we have various components, some of which have parent-child relationships while others are independent. We begin by making an initial API call that returns a true or false flag value. Depending on this value, we decide whether ...

In the past, my code would run smoothly without any issues, but now I am encountering a runtime error even though the code comp

Recently, I started learning TypeScript and encountered an issue while working with Classes. My code was functioning properly before but now it's displaying a runtime error. ...

Troubleshooting the issue with mocking the useTranslation function for i18n in JEST

Currently, I am facing an issue with my react component that utilizes translations from i18next. Despite trying to create tests for it using JEST, nothing seems to be getting translated. I attempted to mock the useTranslation function as shown below: cons ...

Hiding the keypad on an Android device in an Ionic app when user input is detected

I am currently utilizing the syncfusion ej2 Calendar plugin for a datepicker, but I am only using options such as selecting ranges like today, 1 month, or last 7 days from the plugin itself. The plugin provides dropdown options when the calendar is trigger ...

Typescript validation for redundant property checks

Why am I encountering an error under the 'name' interface with an excess property when using an object literal? There is no error in the case of a class, why is this happening? export interface Analyzer { run(matches: MatchData[]): string; } ...

Prevent clicking outside the bootstrap modal in Angular 4 from closing the modal

Just starting out with angular4 and incorporating bootstrap modal into my project. I want the modal to close when clicking outside of it. Here's the code snippet: //in html <div bsModal #noticeModal="bs-modal" class="modal fade" tabindex="-1" rol ...

Validation of Single Fields in Angular Reactive Forms

When I validate a reactive form in Angular, I expect the error message to show up beneath the invalid field whenever incorrect data is entered. <form (ngSubmit)=sendQuery() [formGroup]="form"> <div *ngFor='let key of modelKeys&ap ...

Issue "unable to use property "useEffect", dispatcher is undefined" arises exclusively when working with a local npm package

I am currently in the process of creating my very own private npm package to streamline some components and functions that I frequently use across various React TypeScript projects. However, when I try to install the package locally using its local path, ...

Error message: When using Vue CLI in conjunction with Axios, a TypeError occurs stating that XX

I recently started working with Vue.js and wanted to set up a Vue CLI project with Axios for handling HTTP requests. I came across this helpful guide which provided a good starting point, especially since I plan on creating a large project that can be reus ...

Encountering the ExpressionChangedAfterItHasBeenCheckedError error during Karma testing

Testing out some functionality in one of my components has led me to face an issue. I have set up an observable that is connected to the paramMap of the ActivatedRoute to retrieve a guid from the URL. This data is then processed using switchMap and assigne ...

Having trouble connecting my chosen color from the color picker

Currently, I am working on an angularJS typescript application where I am trying to retrieve a color from a color picker. While I am successfully obtaining the value from the color picker, I am facing difficulty in binding this color as a background to my ...

The error in Angular states that the property 'length' cannot be found on the type 'void'

In one of my components, I have a child component named slide1.component.ts import { Component, Input, OnInit, EventEmitter, Output } from '@angular/core'; @Component({ selector: 'app-slide1', templateUrl: './slide1.component. ...

What causes error TS2345 to appear when defining directives?

Attempting to transition an existing angular application to typescript (version 1.5.3): Shown below is the code snippet: 'use strict'; angular.module('x') .directive('tabsPane', TabsPane) function TabsPane(ite ...

Display a loading GIF for every HTTP request made in Angular 4

I am a beginner with Angular and I am looking for a way to display a spinner every time an HTTP request is made. My application consists of multiple components: <component-one></component-one> <component-two></component-two> <c ...

Encountering difficulty retrieving host component within a directive while working with Angular 12

After upgrading our project from Angular 8 to Angular 12, I've been facing an issue with accessing the host component reference in the directive. Here is the original Angular 8 directive code: export class CardNumberMaskingDirective implements OnInit ...