Varieties of types within class structures

I've been delving into the world of implementing generic types in classes, but it seems that my understanding of what can and cannot be done with generics is a bit lacking.

Hopefully someone out there can shed some light on this for me!

My Objective

I have created a class designed to register tasks and execute them later based on their IDs. Each taskObject consists of a taskId: string and a callback function

callback: <R>(...args: any) => R
. Tasks are registered during class instantiation.

The main idea is to have type information about the return type of the callback function for each individual task.

Thus, taskId is a literal type, and the callback should be an arbitrary function where I want to infer the return type and then make this return type accessible within the class.

The Code Snippet


type Task<T extends string, V extends <R>(...args: any) => R> = {
  taskId: T,
  callback: V // <-- I'm aiming to resolve this
}

class Taskmanager<T extends string, V extends <R>(...args: any) => R> {

  private callbacks = new Map<T, V>();

  public constructor(tasks: Task<T,V>[]) {
    tasks.forEach(task => {
      this.callbacks.set(task.taskId, task.callback);
    });
  }

  public runCallback(taskId: T) {
    return this.callbacks.get(taskId)?();
  }
}

This is just the initial step → now I have type hints for all registered taskIds.

const tm = new Taskmanager([
  {
    taskId: "hello",
    callback: () => "world" // <-- TS error: Type 'string' is not assignable to type 'R'
  },
  {
    taskId: "circle",
    callback: () => 360 // <-- TS error: Type 'number' is not assignable to type 'R'
  }
]);

// Now I get type hints for `taskId` → "hello" | "circle"
const resHello = tm.runCallback("hello"); // <-- I expect: typeof resHello === "string" --> true
const resCircle = tm.runCallback("hello"); // <-- I expect: typeof resCircle === "number" --> true

Check out the TypeScript Playground link

Although I successfully limited the selection to only between hello and circle as parameters in runCallback, I encountered TypeScript errors when declaring the callback functions in the class constructor.

// TS Error for taskId: "hello"
Type 'String' is not assignable to type 'R'.
  'R' could take on any unrelated type that may not be a 'String'.

Is there a feasible way to automatically infer the return type of each callback function? Is this even achievable?

I did browse through several other questions and answers on StackOverflow, but unfortunately, I haven't been able to extract anything useful yet.

I apologize if this question seems trivial. Sometimes working with TS can definitely drive one crazy...

Thank you! 🙌

Answer №1

Considering that the callbacks will be called without any arguments, it is advisable to constrain V to () => any, rather than <R>(...args: any) => R (which introduces a form of generics that may not be implementable properly). Alternatively, you could just use R and have callback as type () => R, but for now, let's make the necessary minimum changes:

type Task<K extends string, V extends () => any> = {
  taskId: K
  callback: V
}

Since you intend to provide an array of entries to the constructor of Taskmanager, we need to add a generic type T to represent elements in that array like so:

class Taskmanager<const T extends Task<string, () => any>> {

  private callbacks = new Map<string, () => any>;

  public constructor(tasks: readonly T[]) {
    tasks.forEach(task => {
      this.callbacks.set(task.taskId, task.callback);
    });
  }

I defined it as a const type parameter because we want the compiler to infer string literal types for the taskId properties instead of just string.

Also note that I kept callbacks as a Map<string, ()=>any> as making it specific to

T</code wouldn't preserve the relationship between keys and values. However, should you require something more specialized with <code>Map
, custom types can be implemented, although it may be excessive in this scenario. For simplicity, we're using () => any and exercising caution when implementing runCallback().

The implementation of runCallback() appears as follows:

  public runCallback<K extends T["taskId"]>(taskId: K):
    ReturnType<Extract<T, { taskId: K }>["callback"]> {
    return this.callbacks.get(taskId)!();
  }
}

This method signature may seem complex, but given that T is a union of subtypes of Task<string, ()=>any>, these constraints are necessary. By restricting K to the union of taskId properties of T, we define a suitable return type leveraging TypeScript utilities like Extract and ReturnType. Ensuring non-nullness when retrieving from the map is crucial to prevent errors due to the type being inferred as any.


Let's proceed with testing:

const tm = new Taskmanager([
  {
    taskId: "hello",
    callback: () => "world"
  },
  {
    taskId: "circle",
    callback: () => 360
  }
]);
/* const tm: Taskmanager<{
    readonly taskId: "hello";
    readonly callback: () => string;
} | {
    readonly taskId: "circle";
    readonly callback: () => number;
}> */

const res = tm.runCallback("hello");
// const res: string
console.log(res.toUpperCase()) // "WORLD"

All seems well. The constructor call is successfully processed, and the typing information embedded within tm aids in correctly inferring the output type of

tm.runCallback("hello")
as string.

Playground link to code

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

Retrieve a specific div within another div by targeting both parent and child classes dynamically

I am working with the following structure: <div class="fc-event lifeguard"> <div class="fc-event-inner"> </div> </div> Inside a javascript function, I am trying to achieve this: $('.lifeguard').find('.fc- ...

What is the best way to retrieve the parent of the initial jQuery object?

At the bottom of a lengthy table containing multiple button.enter-match elements, I am using the following code: $("button.enter-match") .button() .on('click', function() { $("form.enter-match").dialog({ modal: true, height: ...

Clearing up memory in ThreeJS application

I'm facing challenges with memory management in my ThreeJS application. I know there are already a few queries regarding this issue: freeing memory in three.js Freeing memory with threejs Three.js Collada - What's the proper way to dispose() a ...

Activate onchange when including/excluding an option in a dropdown menu (ReactJs)

I need to activate a function whenever a new option is added to a select element. The following code shows the select element: <select name="projects" id="projects" onChange={handleChange}> {Object.keys(items.project ...

Creating a responsive HTML, CSS, and JavaScript popup: A step-by-step guide

I'm facing a challenge in making my HTML, CSS, and JavaScript popup responsive. Despite looking into various resources, I haven't been able to find a solution that fits my code. However, I am confident that it is achievable to make it responsive! ...

The ASPX validation will not be able to access the .js file

I am facing an issue with client-side validation using JavaScript (.js). Despite linking the path in the head section, the ASP file doesn't seem to reach the JavaScript file. <head runat="server"> <meta http-equiv="Content-Type" content="tex ...

What is the best way to locate a shadow root child when waiting for it to appear?

I'm attempting to use Selenium to capture a screenshot of a webpage that contains an img tag nested inside a shadow root. In order to achieve this, I need to ensure that the img element is present before proceeding. Typically, I would employ the foll ...

I was confused about the distinction between the https.get() and https.request() functions in the Node.js npm package for https

// # Exciting Nodejs Programs! const https = require('https'); https.get('https://www.google.com/', (res) => { console.log('statusCode:', res.statusCode); console.log('headers:', res.headers); res.on ...

Using Leaflet JS to implement multiple waypoints on a map

I am trying to create a route with multiple waypoints without hardcoding any data. My waypoints array should dynamically include latitude and longitude values. How can I achieve this? var data = [ { "title": 'Chennai', " ...

Sequelize Error: Cannot call User.hasMany as a function

I am experiencing a strange issue with my User and Client models. The User model has a relationship with the Client model, where User has many Clients. I am currently referencing the Sequelize documentation for associations at this link: Upon running the ...

Generating a new POST header

I have encountered a challenge: I need to transfer an array to another page without using AJAX. My goal is to either redirect the user to the new page or open a popup displaying the new page, all while ensuring the array data is sent via a POST request. C ...

How about using take(1) in conjunction with an Observable<boolean>?

After going through this insightful article, I came across the following implementation of a CanActivate check to determine whether the user can navigate to the home page: canActivate(): Observable<boolean> { return this.authQuery.isLoggedIn$.pipe( ...

Leverage ajax to trigger php which then executes python code and retrieves the desired outcome

I've created a website with a complex structure and numerous javascript and php functions. I made the intentional decision to keep it lightweight by avoiding jquery. Now, I want to incorporate a python function that will return a value to the website ...

Incorrectly parsing data in ReactJS leads to errors

After making significant progress, the focus of this question has strayed too far. Therefore, it will not be continued. *===== The issue at hand revolves around fetching data from an API, receiving results, and encountering difficulties when attempting t ...

obtain the position of an element within an array

After entering a value in the input field, an array is generated with a length based on that input. For example, if I enter 3 in the input field, I will have var input = 3 and var sir = [0,1,2]. Can someone please help me figure out why my attempt to find ...

The JavaScript syntax dollar sign

I am currently studying a JavaScript source code and it's my first time writing JavaScript. I find some of the syntax confusing. <script id="source" language="javascript" type="text/javascript"> $(function () { window.onload=function() ...

Looking to bring in a non-ES6 library into VueJS without using export statements

Currently, I am faced with an interesting challenge. For a forthcoming VueJS project, we must utilize a library that is quite outdated but there is simply not enough time to redevelop it. The library is essentially a JavaScript file which consists of num ...

Is it possible to dynamically import a Vue.js component based on a prop value?

Currently, I am utilizing the Material design Icons library specifically made for Vue.js. In this library, each icon is a standalone file component within its designated folder. Within my project, I have a separate component called ToolbarButton, which in ...

Angular 6 methods that handle Observable data from HTTP GET requests complete execution before constructing the final Data

My goal is to retrieve data from a REST endpoint and then construct a List of instances to return. However, I'm facing an issue where the HTTP GET function returns an Observable method that always results in an empty list before populating it with dat ...

transform an array encoded in base64 format into a JSON object

I am encountering an issue where the base64 array I'm trying to send to an API is coming up empty. Here's a breakdown of what I'm doing: Firstly, I have an array of files with images in my code: [0: File {name: '766_generated.jpg' ...