Decorating a class method with a versatile class method decorator

I'm currently attempting to make a class method decorator that functions perfectly with a regular class method compatible with a generic class method.

Here's the code in question at this typescript playground link:

abstract class WorkerBase {
  log(...arguments_: unknown[]) {
    console.log(arguments_);
  }
}

const span =
  <
    This extends WorkerBase,
    Target extends (this: This, ...arguments_: unknown[]) => Promise<unknown>,
  >() =>
  (target: Target, context: ClassMethodDecoratorContext<This, Target>) =>
    async function replacementMethod(
      this: This,
      ...originalArguments: unknown[]
    ) {
      this.log(['called', this.constructor.name, context.name]);
      return target.call(this, ...originalArguments);
    };

class ExterneWorker extends WorkerBase {
  @span()
  protected async get<Return>() {
    return undefined as unknown as Return;
  }
}

Compiling this code returns the following error:

Decorator function return type '(this: ExterneWorker, ...originalArguments: unknown[]) => Promise<unknown>' is not assignable to type 'void | (<Return>() => Promise<Return>)'.
  Type '(this: ExterneWorker, ...originalArguments: unknown[]) => Promise<unknown>' is not assignable to type '<Return>() => Promise<Return>'.
    Type 'Promise<unknown>' is not assignable to type 'Promise<Return>'.
      Type 'unknown' is not assignable to type 'Return'.
        'Return' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.

The issue arises when trying to use the span decorator with a generic class method. While it works well with non-generic methods, it encounters problems with generics. Interpreting TypeScript's error message has proven challenging for me. It seems like there might be an issue with how I've defined the Return parameter within the span decorator definition.

Answer №1

The issue primarily lies in the fact that the get() method's type is defined as

<R>() => Promise<R>
, indicating that it can return any type based on the caller's preference. This kind of method implementation poses a significant safety risk and, when analyzed within a robust type system, is essentially equivalent to () => never. The concept of the never type, designated as the bottom type, can be assigned to any other type, highlighting its stark contrast with the unknown type. Therefore, () => unknown cannot be compatible with the type of the get() method, leading to compiler errors.

For instance, calling worker.get<string>() returns a Promise<string>, while worker.get<number>() produces a Promise<number>. This scenario suggests that worker.get() somehow generates a Promise encompassing both string and number types simultaneously, which is logically impossible since there are no values that align with both string and number. In essence, it equates to never.


If you still wish to utilize this type, it implies that your span() function must accept methods of diverse function types, even those deemed unsafe such as () => never and

<R>() => Promise<R>
. A straightforward solution would be to employ an insecure type... the any type:

const span = <
  Target extends (...args: any) => Promise<any>,
>() =>
  (
   target: Target, 
   context: ClassMethodDecoratorContext<ThisParameterType<Target>, Target>
  ) =>
    async function replacementMethod(
      this: ThisParameterType<Target>,
      ...originalArguments: Parameters<Target>
    ) {
      console.log(['called', context.name]);
      return target.call(this, ...originalArguments);
    };

class ExterneWorker extends WorkerBase {
  @span() // acceptable
  async get<Return>() {
    return undefined as unknown as Return;
  }
}

This setup closely resembles yours but incorporates the ThisParameterType utility type for determining the mentioned This type rather than deducing it implicitly.

While acknowledging the unsafety of any, using (...args: any) => any accommodates various functions without concern for argument or return types. Eliminating any entirely may prove challenging, especially when interfacing with already risky types like <R>() => R.

Playground link for code demonstration

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

Automatically identify the appropriate data type using a type hint mechanism

Can data be interpreted differently based on a 'type-field'? I am currently loading data from the same file with known type definitions. The current approach displays all fields, but I would like to automatically determine which type is applicab ...

Setting a default value for a complex prop in Vue through Type-based props declarations

I'm struggling with this issue: Argument of type 'HelloWorldProps' is not assignable to parameter of type 'InferDefaults<LooseRequired<HelloWorldProps>>'. Types of property 'complexProp' are incompatible.ts( ...

Instance of "this" is undefined in Typescript class

After stumbling upon this code online, I decided to try implementing it in TypeScript. However, when running the code, I encountered an error: Uncaught TypeError: Cannot set property 'toggle' of null @Injectable() export class HomeUtils { p ...

Angular: Populating a date field using a dropdown menu selection

Imagine there's a dropdown menu in my application, with options like "WORK", "RELEASE", and "OPEN". There's also a calendar field that is initially empty. When I choose the option "RELEASE" from the dropdown menu, I want it to automatically selec ...

The playwright signs in to the application through the cached session in global-setup.ts, where the wait for the selector times out when DEBUG=0 but does not time out when DEBUG=

*Updates 22.6.2022 / Identified a recurring issue with OAuth on another site, where globalSetup fails to function properly on the OAuth domain. 21.6.2022 / Analysis from Trace.zip reveals that the OAuth login URL is correct, but the screenshot depicts a bl ...

Setting the default value in a Reactive form on the fly: A step-by-step guide

When creating a table using looping, I need to set the default value of my Reactive Form to `Repeat` if the loop value matches a particular character, otherwise I want it to be empty. Here is my code: typescript rDefault:string = ""; create(){ ...

How can we initiate an AJAX request in React when a button is clicked?

I'm fairly new to React and I'm experimenting with making an AJAX call triggered by a specific button click. This is how I am currently using the XMLHttpRequest method: getAssessment() { const data = this.data //some request data here co ...

Tips for quietly printing a PDF document in reactjs?

const pdfURL = "anotherurl.com/document.pdf"; const handleDirectPrint = (e: React.FormEvent) => { e.preventDefault(); const newWin: Window | null = window.open(pdfURL); if (newWin) { newWin.onload = () => ...

Transitioning Apollo Server from version 3 to version 4 within a next.js environment

Previously in v3, you could define "createHandler" like this: export default async (req, res) => { await startServer; await apolloServer.createHandler({ path: "/api/graphql", })(req, res); }; However, in v4, this is no longer possi ...

Creating a JSON schema for MongoDB using a TypeScript interface: a step-by-step guide

In order to enhance the quality of our data stored in MongoDB database, we have decided to implement JSON Schema validation. Since we are using typescript in our project and have interfaces for all our collections, I am seeking an efficient method to achie ...

Replacing a push operation in JavaScript with a splice operation

Upon entering a screen, 5 promises are automatically loaded using promise.all. The issue is that they are executed in a random order, and within each function, I use a push to store the information. The problem arises when I need to change the push to a s ...

EmotionJS Component library's Component is not able to receive the Theme prop

I am in the process of developing a component library using Emotion and Typescript. However, I have encountered an issue when trying to import the component into a different project that utilizes EmotionJS and NextJS - it does not recognize the Theme prop. ...

What is the best way to display API error messages to the user?

When a user tries to upload a file that is not an image, I need to display the error message returned from a REST API. The JSON response received from the API will look something like this: { "publicError": "Could not be uploaded, it is not an image! ...

Changes on services do not affect the Angular component

Currently facing an issue with my Angular assignment where changing an element's value doesn't reflect in the browser, even though the change is logged in the console. The task involves toggling the status of a member from active to inactive and ...

The parent component remains visible in the Angular router

Within my appComponent, I have a layout consisting of a toolbar, footer, and main content. The main content utilizes the router-outlet directive structured as follows: <div class="h-100"> <app-toolbar></app-toolbar> < ...

adjust the child component by directly accessing its reference

Struggling to update a child component from the parent component using its reference, but running into some issues. Here's what I've attempted so far: class MainApp extends React.Component<any, any> { construct ...

The utilization of `ngSwitch` in Angular for managing and displaying

I am brand new to Angular and I'm attempting to implement Form Validation within a SwitchCase scenario. In the SwitchCase 0, there is a form that I want to submit while simultaneously transitioning the view to SwitchCase 1. The Form Validation is fun ...

Populating the DOM with a mix of strings and HTMLDivElements by iterating through an array using *ngFor

I have a specific layout requirement that needs to resemble this example: https://i.sstatic.net/4kP2q.png The desired layout should utilize CSS properties like display: grid; someFunction(data) { this.data = data; ...

Facing issues with integrating Mixpanel with NestJS as the tracking function cannot be located

While utilizing mixpanel node (yarn add mixpanel) in conjunction with NestJS, I have encountered an issue where only the init function is recognized. Despite calling this function, I am unable to invoke the track function and receive the error message: Ty ...

Disabling dynamic color updates upon refresh

I am currently using chartjs and I want to generate new random colors each time the page is refreshed. However, I need these colors to remain fixed and not change after a page refresh or reload. Below is the function I am working with: getRandomRgb() { ...