Creating a function that leverages both generic types and variadic arguments for enhanced functionality

Imagine a more streamlined design:

type BaseResponse<T> = {
  success: boolean;
  data?: T;
}

Consider the following function, which merges multiple BaseResponse objects into one:

zip<T1, T2>(response1: BaseResponse<T1>, response2: BaseResponse<T2>): BaseResponse<T1 & T2> {
    let success = true;
    let data: Partial<T1 & T2> = {};

    if (!response1.success) success = false;
    else data = {...data, ...response1.data};

    if (!response2.success) success = false;
    else data = {...data, ...response2.data};

    return {
        success,
        data: success ? data as any : undefined,
    };
}

This approach is effective for handling two arguments. Is there a way to generalize it for an arbitrary number of arguments T1, T2, ..., TN?

In terms of implementation, utilizing a for loop for iterating through arguments could simplify the process. However, I am interested in how the function signature would need to be adapted to accommodate this flexibility.

Answer №1

Your inquiry seems to revolve around the call signature, assuming you have the capability to manage the implementation on your own.

It's worth noting that the UnionToIntersection<T> type discussed in Transform union type to intersection type may not be the best solution for your scenario. If any of the inputs for your BaseResponse<T> have a union type for T, using UnionToIntersection could lead to erroneous intersections. Therefore, calling zip() on a BaseResponse<X | Y> should yield a BaseResponse<X | Y>, not BaseResponse<X & Y>.

For instance, here is the expected behavior:

declare const r0: BaseResponse<{ a: string }>;
declare const r1: BaseResponse<{ b: number }>;
declare const r2: BaseResponse<{ c: boolean }>;
declare const r3: BaseResponse<{ d: Date } | { e: HTMLElement }>;
const zipped = zip(r0, r1, r2, r3);
/* const zipped: BaseResponse<
   {a: string} & {b: number} & {c: boolean} & ({d: Date} | {e: HTMLElement})
                     still a union ----------> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>*/

To achieve this, manipulating the tuple of inputs directly and creating a TupleToIntersection<T> utility type might be necessary. Here is a possible implementation:

type TupleToIntersection<T extends any[]> =
    { [I in keyof T]: (x: T[I]) => void } extends 
    Record<number, (x: infer I) => void> ? I : never;

This works similarly to UnionToIntersection, positioning each tuple element contravariantly and then inferring a single type from that position to obtain the intersection.

The call signature of zip() would then look like this:

declare function zip<T extends any[]>(
    ...response: { [I in keyof T]: BaseResponse<T[I]> }
): BaseResponse<TupleToIntersection<T>>;

You can verify its functionality by testing as described above.

Access the Playground link to view the code

Answer №2

There may be a more efficient approach to solving this without relying on the UnionToIntersection type. However, the following code should suffice:

I came across the UnionToIntersection type from this source: link

type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
  x: infer I
) => void
  ? I
  : never;

function zipSpread<T extends BaseResponse<any>[]>(
  ...responses: T
): BaseResponse<UnionToIntersection<T[number]['data']>> {
  // your implementation logic here.
}

Answer №3

If you want to use the UnionToIntersection utility, make sure to also incorporate the {success: false} scenario:

Check it out here!

type BaseResponse<T> = {
  success: boolean;
  data?: T;
}

type UnionToIntersection<U> = (U extends any ? (arg: U) => any : never) extends ((arg: infer I) => void) ? I : never
type Simplify<T> = {[K in keyof T]: T[K] extends Function ? T[K] extends (...args: infer B) => infer A ? (...args: B) => Simplify<A> : never : Simplify<T[K]>} extends infer A ? A : never;

function zip<T extends object, R extends BaseResponse<T>, const A extends R[]>(...responses: A): Simplify<UnionToIntersection<A[number]>> {

    const success = responses.every(response => response.success);

    if(!success) return {success} as any;

    const data = {};

    for(const response of responses){
        Object.assign(data, response);
    }

    return {
        success,
        data,
    } as any;
}

declare const r1: BaseResponse<Readonly<{test: 'test'}>>;
declare const r2: BaseResponse<Readonly<{test2: 2}>>;

const d = zip({success: true, data:{test: 'test'}}, {success: true, data:{test1: 1}});
/*
const d: {
    readonly success: true;
    readonly data: {
        readonly test: "test";
        readonly test1: 1;
    };
}
*/
const d2 = zip(r1, r2);
/*
const d2: {
    success: boolean;
    data?: {
        readonly test: "test";
        readonly test2: 2;
    } | undefined;
}
*/

Answer №4

To solve the issue at hand, one could consider utilizing a union approach such as

type MergeTypes<U> = 
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

type BaseResponse<T> = {
  success: boolean;
  data?: T;
};

function combineResponses<T extends any[]>(
  ...responses: { [K in keyof T]: BaseResponse<T[K]> }
): BaseResponse<MergeTypes<T[number]>> {
  let success = true;
  let data: Partial<MergeTypes<T[number]>> = {};

  for (const response of responses) {
    if (!response.success) {
      success = false;
    } 
    if (response.data) {
      data = { ...data, ...response.data };
    }
  }

  return {
    success,
    data: success ? (data as MergeTypes<T[number]>) : (data ? data as MergeTypes<T[number]> : undefined),
  };
}

This method allows for the combination of different types and their subsequent inclusion in the base response.

For an illustration of its usage, refer to this link.

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

Jasmine : Techniques for monitoring a method callback using method.then()

Within my Angular 4.0.0 application, I have a method called in my component. This method is invoked within a service: this.myService.myMethod(param).then(any => { console.log("success case"); }) .catch(error => { console.log("error"); }); ...

How to reference an object from an external file in TypeScript using Ionic 2 and Angular 2

I am currently developing a mobile application with Ionic2 and have integrated a simple online payment service called Paystack for processing payments. The way it operates is by adding a js file to your webpage and then invoking a function. <script> ...

Transform the look of the GroupLabels in MUI Autocomplete components within a React environment

Looking for some assistance with customizing the appearance of group labels in the Autocomplete component from React Material-UI on my website. I've successfully changed the List-Elements to have a dark background, as shown in this image: https://i.ss ...

Cease the execution of the express function once a response has been sent

Recently delving into the world of .js and .ts, I've been exploring express.js. Take a look at the snippet below: private post = async ( request: express.Request, response: express.Response, next:express.NextFunction) => { await this.myfu ...

Creating a custom type declaration for .lottie files in Next.js

https://i.sstatic.net/go6pV.png I've successfully integrated the module with no "can't find module" errors, and my code is functioning correctly. However, the main problem lies in the type declarations error, which prevents my code from recogni ...

Angular 4 is unable to attach to 'formGroup' as it is not recognized as a valid property of 'form'

As a beginner with Angular 4, I decided to explore model driven forms in Angular 4. However, I keep encountering this frustrating error. Template parse errors: Can't bind to 'formGroup' since it isn't a known property of 'form ...

Tips for managing the data type of a bound value through ngModel: preventing number distortion to string

I posted a query and managed to solve it. However, I observed that even though the provided data consists of objects defined like this: export interface GisPoint { e: number; n: number; } when a user inputs a value, the original content changes from { e: ...

I'm facing difficulty transferring information to another component

I'm currently using Next.js with TypeScript and Material UI. I have created a component called MyOrders and I am trying to pass data from MyOrders to MyOrderItem. However, I am encountering an issue where I am unable to pass the data to MyOrderItem. ...

Retrieve the document id along with the corresponding data from a collection

Retrieving data from the collection leads and displaying all documents in an HTML table. Upon clicking, I need to extract the document ID of the clicked item as it serves as the sole primary key. P.S I am able to obtain records of all documents in an arra ...

Conceal a row in a table using knockout's style binding functionality

Is it possible to bind the display style of a table row using knockout.js with a viewmodel property? I need to utilize this binding in order to toggle the visibility of the table row based on other properties within my viewmodel. Here is an example of HTM ...

Default exports are not supported in TypeScript

I'm encountering issues with my Laravel + Vite + Vue 3 project. I followed the installation instructions in the documentation and everything works fine when the project is separated from Laravel and Vite. However, I'm facing a problem where TypeS ...

What is the best way to include TypeScript definition files in version control?

I am working on a basic asp.net core web application and have integrated javascript libraries using libman. Now, I am looking to incorporate typescript, therefore I have obtained typescript definition files for the libraries via npm. For example: npm ins ...

Exploring Typescript within React: Creating a property on the current instance

Within my non-TypeScript React component, I previously implemented: componentWillMount() { this.delayedSearch = _.debounce((val) => { this.onQuerySearch(val); }, 1000); } This was for debouncing user input on an input field. The corres ...

I'm having some trouble grasping the concept of the useReducer hook

I've been attempting to modify a value from a select menu. My current code is functional, but I'm wondering if switching to useReducer would be more efficient. I made an attempt at it, but unfortunately, I couldn't get it to work. The docume ...

What is the method for generating a data type from an array of strings using TypeScript?

Is there a more efficient way to create a TypeScript type based on an array of strings without duplicating values in an Enum declaration? I am using version 2.6.2 and have a long array of colors that I want to convert into a type. Here is what I envision: ...

SystemJS is loading classes that are extending others

In my Angular2 application, I have two classes where one extends the other. The first class is defined in the file course.ts (loaded as js) export class Course { id:string; } The second class is in schoolCourse.ts (also loaded as js) import {Cours ...

The default value is not appearing in the Select Box when using [(ngModel)] in Angular 2

I have encountered an issue with the code snippet below. When I use variable binding "[(ngModel)]", the default option "Title*" is not visible. However, if I remove it, the first option is shown as selected by default. <select name="title" id="title ...

Steps for positioning grid lines behind the flex container

Currently, I have a flex container with several flex items arranged from top to bottom. My goal is to create light grey color lines behind this flex container to achieve a design similar to this. I attempted to add another flex container on top of this on ...

Angular local storage override problem

For system authentication purposes, I am storing the access token in the local storage. When User-A logs in, their access token is saved in the local storage without any issues. However, if User-B opens another tab of the same project and logs in, their ...

Error alert: Ionic detected a SyntaxError due to a missing closing parenthesis on the emulator

When using Ionic, I encountered an error Uncaught SyntaxError: missing ) after argument list on the emulator, but everything runs smoothly with the serve command: Fetch(what, callbackf) { return this.woo.getAsync(what).then( (result)=> { th ...