Is it possible for TypeScript to preserve the return type while consolidating multiple classes or objects of functions in a reducer method?

Describing my issue with the title was challenging, but here it is:

I have several objects that follow this structure:

type TUtilityFunction = {[key: string]: <T>(a: T, b: any) => T}

For example:

class UtilityA{
  DoSomeWork = function (arg1: SomeCustomType, arg2: string){
    // do some work
    return arg1;
  }

class UtilityB{
  DoSomeOtherWork = function (arg1: SomeCustomType, arg2: number){
    // do some work
    return arg1;
  }
}

I want to merge these two classes into one while preserving intellisense with the new consolidated object.

The resulting object would combine functionalities of both previous classes:

{
  DoSomeWork: (arg1: SomeCustomType, arg2: string) => SomeCustomType,
  DoSomeOtherWork: (arg1: SomeOtherCustomType, arg2: number) => SomeCustomType
}

I attempted a solution similar to this Is it possible to infer return type of functions in mapped types in TypeScript?

but it focused on a single object of functions, whereas I have multiple. My best effort so far looks like this:

export const combineUtilities = function <
    TUtilities extends {
        [TKey in keyof TUtilities ]: Record<keyof TUtilities [keyof TUtilities ], TUtilityFunction>;
    }
>(reducers: TUtilities ): Record<keyof TUtilities [keyof TUtilities ], TUtilityFunction> {
    return (Object.keys(reducers) as Array<keyof TUtilities >).reduce(
        <K extends keyof TUtilities >(
            nextReducer: {[key in keyof TUtilities [keyof TUtilities ]]: TUtilityFunction},
            reducerKey: K
        ) => {
            return {...nextReducer, ...reducers[reducerKey]};
        },
        {} as Record<keyof TUtilities [keyof TUtilities ], TUtilityFunction>
    );
};

Although TypeScript allows me to write this code, when I try to use the method:

const result = combineUtitilies({prop1: new UtilityA(), prop2: new UtilityB()});

the resulting type is:

const result: Record<never, TUtilityFunction>

which seems logical, but I'm stuck on how to infer the end result or somehow infer each utility class entering the combine method. The number of utility classes can vary as arguments but will always be at least 2. Is this even achievable? Any advice is greatly appreciated!

Update

The example I provided earlier was simplified to highlight the core problem. As mentioned by motto, simply spreading the two classes into a new object worked. However, I noticed when working with my actual code, I still encountered the "never" type.

This might be due to having a private variable with the same name in both classes. Now that I've resolved that, I need to find a way forward. This private variable is passed through the constructor and acts as a config variable. To elaborate further, imagine the two classes looking like this:

class UtilityA{

  private readonly config: TSomeConfigType;

  constructor(config: TSomeConfigType) {
    this.config = config;
  }

  DoSomeWork = function (arg1: SomeCustomType, arg2: string){
    // do some work
    return arg1;
  }

class UtilityB{

  private readonly config: TSomeConfigType;

  constructor(config: TSomeConfigType) {
    this.config = config;
  }

  DoSomeOtherWork = function (arg1: SomeCustomType, arg2: number){
    // do some work
    return arg1;
  }
}

When running:

const result = {...new UtilityA({}), ...new UtilityB({})}

the result is:

const result: {}

Which makes sense because it's combining two instances of config with the same property, as mentioned by motto. Sometimes this config property may be of a different type. So now I'm contemplating the best approach to merge the utilities while keeping each instance of config separate. Perhaps the combine function needs to dynamically rename each config instance to a unique name. But maybe that's excessive.

What would be a good strategy for this?

Answer №1

Perhaps you're making things a bit too complex.

Basically, what you're doing is finding the commonalities between the arguments passed to combineUtilities(...) (excluding cases where different utility classes have members with the same names).

You can achieve this with minimal code:

const result = { ...(new UtilityA()), ...(new UtilityB()) }

/* Type inference works correctly:
const result: {
    DoSomeOtherWork: (arg1: SomeCustomType, arg2: number) => SomeCustomType;
    DoSomeWork: (arg1: SomeCustomType, arg2: string) => SomeCustomType;
}
*/

The downside is that because each class can name its members differently, achieving the level of type safety you desire might be challenging. When using object spreading in the .reduce(..) call, all class definitions will be included regardless of their relevance to specific type contracts. To ensure proper type restriction for the exposed methods in your combined reducer, consider imposing limitations on method names.

Update to Address Update

It seems like you're trying to fit a square peg into a round hole by forcing a has-a relationship into an is-a relationship.

Simplifying things further could help. Utilize classes for encapsulation by pre-instantiating your utility classes, ensuring appropriate binding of this, and explicitly exposing required methods:

const utilA = new UtilityA(config);
const utilB = new UtilityB(config);

const result = {
    DoSomeWork: utilA.DoSomeWork.bind(utilA), // or bind in the constructor ...
    DoSomeOtherWork: utilB.DoSomeOtherWork.bind(utilB)
}
/* IntelliSense correctly derives the type:
const result: {
    DoSomeWork: (arg1: SomeCustomType, arg2: string) => SomeCustomType;
    DoSomeOtherWork: (arg1: SomeCustomType, arg2: number) => SomeCustomType;
}
*/

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

Resolving Hot-Reload Problems in Angular Application Following Upgrade from Previous Version to 17

Since upgrading to Angular version 17, the hot-reload functionality has been causing some issues. Despite saving code changes, the updates are not being reflected in the application as expected, which is disrupting the development process. I am seeking ass ...

Storing information from JSON into an object

I am encountering an issue regarding transferring data from JSON to an object. Although this solution is functional, it is not entirely satisfactory. Take a look at the service. Is there an improved method for handling data conversion from this JSON to an ...

Inefficiency in POST method prevents data transmission to MongoDB

I've developed a MERN application and now I'm testing the backend using the REST client vscode extension. This is how it looks: `POST http://localhost:4000/signup Content-Type: application/json { "email": "<a href="/cdn-cgi ...

Whenever I attempt to make changes to the React state, it always ends up getting reset

Currently, I am attempting to utilize Listbox provided by Headless UI in order to create a select dropdown menu for filtering purposes within my application. However, the issue I have encountered is that whenever I update my "selectedMake" state, it revert ...

generate a fresh array with matching keys

Here is an example array: subjectWithTopics = [ {subjectName:"maths", topicName : "topic1 of maths " }, {subjectName:"maths", topicName : "topic2 of maths " }, {subjectName:"English", topicName : &quo ...

Is there an automatic bottom padding feature?

Currently, I am facing a challenge in fitting the loader into the container without it being overridden by the browser. Using padding-bottom is not an ideal solution as it results in the loader appearing un-resized and unprofessional. Any suggestions or co ...

Include additional information beyond just the user's name, profile picture, and identification number in the NextAuth session

In my Next.js project, I have successfully integrated next-auth and now have access to a JWT token and session object: export const { signIn, signOut, auth } = NextAuth({ ...authConfig, providers: [ CredentialsProvider({ async authorize(crede ...

Required Field Validation - Ensuring a Field is Mandatory Based on Property Length Exceeding 0

When dealing with a form that includes lists of countries and provinces, there are specific rules to follow: The country field/select must be filled out (required). If a user selects a country that has provinces, an API call will fetch the list of provinc ...

Implementing query parameters in a Deno controller

I developed a couple of APIs for a Deno Proof of Concept. This is the route implementation: const router = new Router() router.get('/posts', getPosts) .get('/posts/:id', getPostsById) In the second route, I successfully retriev ...

Is there a way to reset the yAxes count of a chart.js chart in Angular when changing tabs?

I am currently using chart.js within an Angular framework to visually display data. Is there any method available to reset the y-axis data when changing tabs? Take a look at this Stackblitz demo for reference. Upon initial loading of the page, the data ...

What is the best way to implement custom sorting for API response data in a mat-table?

I have been experimenting with implementing custom sorting in a mat-table using API response data. Unfortunately, I have not been able to achieve the desired result. Take a look at my Stackblitz Demo I attempted to implement custom sorting by following t ...

Error encountered: TypeScript module 'angularfire2/interfaces' not found in Ionic 3 with angularfire2-offline plugin

Encountering an error while trying to set up angularfire2-offline: [16:02:08] typescript: node_modules/angularfire2-offline/database/database.d.ts, line: 2 Cannot find module 'angularfire2/interfaces'. L1: import { Angula ...

The method of implementing an index signature within TypeScript

I'm currently tackling the challenge of using reduce in Typescript to calculate the total count of incoming messages. My struggle lies in understanding how to incorporate an index signature into my code. The error message that keeps popping up states: ...

Utilizing Google OAuth2 API within an Angular2 Typescript Application

Looking to incorporate the Google oauth2 API and Calender API into my Angular2 application. Struggling to find a working sample to guide me through the integration process. Has anyone come across a functioning example? Thanks, Hacki ...

When employing the caret symbol (^) in package.json, it fails to update the minor version

Within my package.json file, there is a line that reads as follows: "typescript": "^4.1.6", The presence of the caret (^) symbol indicates that npm should install a version of TypeScript above 4.1 if available. However, upon checking ...

Mastering Angular Apollo Error Resolution Methods

Hey everyone, I'm facing a problem with apollo-angular and apollo-link-error that I just can't seem to figure out. I've experimented with different approaches but have had no luck catching errors on the client-side of my angular web app. Bel ...

When using Inertia.js with Typescript, an issue arises where the argument types {} and InertiaFormProps{} are not compatible with the parameter type Partial<VisitOptions> or undefined

I set up a brand new Laravel project and integrated Laravel Breeze along with Typescript support. After creating a form (using useForm()) and utilizing the .post() method with one of the options selected (such as onFinish: () =>), I encountered the fol ...

Guide on including a in-browser utility module from single-spa into a TypeScript parcel project

There are 3 TypeScript projects listed below: root-config a parcel project named my-app an in-browser utility module called api All of these projects were created using the create-single-spa command. In the file api/src/lomse-api.ts, I am exporting the ...

What is the best way to document a collection of generic interfaces while ensuring that they adhere to specific

I am currently utilizing a parser toolkit called Chevrotain to develop a query language that offers users the ability to enhance its functionality. Despite having all the necessary components in place, I am facing challenges when it comes to defining types ...

I am attempting to create a multi-line tooltip for the mat-icon without displaying " " in the tooltip

I attempted to create a multiline tooltip using the example below. However, the \n is showing up in the tooltip. I am looking to add a line break like we would with an HTML tooltip. Check out the code here. ...