Tips for creating a versatile object combine function in TypeScript

Is there a way to create a generic function in TypeScript 4.4+ that is equivalent to {...a,...b} operation? Both a and b are records, but their types are unknown in advance. I want to define a generic type that enforces arbitrary functions to perform the {...a,...b} operation.

Here's an example that is not working as expected:

type Rec = Record<string, unknown>
type ExtendRec = <X extends Rec, Y extends X>(x: X) => Y
// or <X extends Rec, Y extends Rec>(x: X) => Y&X
const addA:ExtendRec = <X extends Rec>(x:X) => ({...x, a: 'a'})
const addB:ExtendRec = <X extends Rec>(x:X) => ({...x, b: 'b'})
const addC:ExtendRec = <X extends Rec>(x:X) => ({...x, c: 'c'})
const blank = {} // expected {}
const a = addA(blank) // expected {a:string}, actual:{}
const ab = addB(a) // expected {a:string,b:string}, actual:{}
const abc = addC(ab) // expected {a:string,b:string,c:string}, actual:{}

The error message on each of the addA, addB,addC functions is as follows:

Type '<X extends Rec>(x: X) => X & { c: string; }' is not assignable to type 'ExtendRec'.
  Type 'X & { c: string; }' is not assignable to type 'Y'.
    'X & { c: string; }' may be different from the subtype of constraint 'Record<string, unknown>'.

It's puzzling because if we remove the ExtendRec function annotations, TypeScript can infer object assign operations correctly. However, it seems challenging to define a generic function type that restricts an arbitrary function to that specific extend operation.

Answer №1

In my opinion, there is a need for a generic type (to encompass the properties that will be included) along with a function (to extend the object).

For instance:

type ExtendRec<R extends Record<string, unknown>> = <X>(x: X) => X & R

Following this, everything will work as anticipated:

type ExtendRec<R extends Record<string, unknown>> = <X>(x: X) => X & R

const addA: ExtendRec<{ a: string }> = (x) => ({ ...x, a: 'a'})
const addB: ExtendRec<{ b: string }> = (x) => ({ ...x, b: 'b'})
const addC: ExtendRec<{ c: string }> = (x) => ({ ...x, c: 'c'})

const blank = {} // expected {}
const a = addA(blank) // expected {a:string}
const ab = addB(a) // expected {a:string,b:string}
const abc = addC(ab) // expected {a:string,b:string,c:string}

Playground


Another approach could involve utilizing a merge function incorporating generics in both arguments. addA can simply use that where one argument represents the generic and the other a known type.

type MergeFn = <A extends object, B extends object>(a: A, b: B) => A & B
const merge: MergeFn = (a, b) => ({ ...a, ...b })

const addA = <X extends object>(x: X) => merge(x, { a: 'a'})
const addB = <X extends object>(x: X) => merge(x, { b: 'b'})
const addC = <X extends object>(x: X) => merge(x, { c: 'c'})

Playground

This is somewhat similar to the previous example. In either case, the added type is declared statically when defining addA, and at the time of calling addA, X is inferred.

Note: I've used object instead of Record<string, unknown> for brevity, although the distinction isn't crucial here.

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

My requests and responses will undergo changes in naming conventions without my consent or awareness

Initially, I wrote it in a somewhat general manner. If you require more information, please let me know! This is how my C# class appears when sent/received on the frontend: public class Recipe : ICRUD { public Guid ID { get; set; } ...

TypeError in TypeScript: Unable to find property 'key' in type 'HTMLAttributes<HTMLLIElement>'

While attempting to destructure the key property from an object, TypeScript is raising an error stating that Property 'key' does not exist on type 'HTMLAttributes<HTMLLIElement> However, upon logging the props object using console.log ...

Exploring the potential of TypeScript with native dynamic ES2020 modules, all without the need for Node.js, while also enhancing

I have a TypeScript application that utilizes es16 modules, with most being statically imported. I am now looking to incorporate a (validator) module that is only imported in debug mode. Everything seems to be functioning properly, but I am struggling to f ...

What is the best way to loop through an array that contains a custom data type

When I declared the type: export interface Type{ id: number; name: string; } I attempted to iterate over an array of this type: for(var t of types) // types = Type[] { console.log(t.id); } However, I encountered the following error message: ...

Tips on reordering Angular material tabs on the fly

I am working with a group of 7 tabs using Angular material: <mat-tab-group #tabGroup [selectedIndex]="selectedIndex"> <mat-tab label="Tab 1">Content 1</mat-tab> <mat-tab label="Tab 2">Content 2</mat-tab> <mat-t ...

Is there a way to trigger the click event in the week view of an Angular 2+ calendar?

https://i.sstatic.net/Vx2x8.png HTML Templates <mwl-calendar-week-view [viewDate]="viewDate" [refresh]="refresh" (click)="weekDayClick($event)"> </mwl-calendar-week-view> In the component file weekDayCl ...

Why is the authentication service failing to remember user authentication?

Despite having an auth guard and auth service that are functioning correctly, I encounter the issue of being logged out when attempting to access my application in a new browser tab. Each time a new tab is opened, I am prompted to log in again. Ideally, th ...

JavaScript Tutorial: Adding Custom Metadata to PDFs

Does anyone know of a JavaScript package that can assist in adding custom metadata to a PDF file? ...

Unending loop caused by nested subscriptions - Angular / RxJS

I am currently implementing a nested subscribe operation, although I am aware that it may not be the most efficient method. Here is an example of what I have: this.route.params.subscribe((params) => { this.order$ .getMa ...

Transform the Standard class into a generic one in typescript

I've created a class that can take JSON objects and transform them into the desired class. Here's the code: import {plainToClass} from "class-transformer"; import UserDto from "../../auth/dto/user.dto"; class JsonConverter { ...

Another return payload failing to retrieve the return value

I'm currently facing an issue where a function that should return a value is not being passed on to another function. Below is the code snippet in question: public _getProfileToUpdate() { return { corporateId: this.storeService.setStoreData().p ...

Creating a mongoDB query that matches elements in an array of subdocuments with elements in a Typescript Array

In my database, I have stored various Events using mongoDB. Each event comes with multiple fields, including an array of genres, which consists of subdocuments like {genre and subGenre}. For instance, an event could be classified as {genre: "music", subGe ...

The customization of primary and secondary palettes in React MUI5 with TypeScript theme is restricted and cannot

Our design team put together numerous custom palettes and additional properties. While this posed no problem in JS, transitioning to TS has proven to be quite challenging. I managed to prevent any errors from being thrown in the createTheme file, but using ...

Steps for Creating a Private Constructor in a Module while Allowing External Construction

I am looking for a way to restrict the construction of a class within a module to only be possible through a helper function from that same module. This would prevent any external users of the class from constructing it without using the designated helper ...

Create a dynamic function that adds a new property to every object in an array, generating unique values for

On my server, I have a paymentList JSON that includes date and time. Utilizing moment.js, I am attempting to create a new property called paymentTime to store the time data, but it seems to not update as expected. this.paymentList.forEach(element => ...

VSCode prioritizes importing files while disregarding any symbolic links in order to delve deeper into nested node

I'm encountering a problem with VSCode and TypeScript related to auto imports. Our application includes a service known as Manager, which relies on certain functions imported from a private npm package called Helpers. Both Manager and Helpers make us ...

Retrieving key values from an interface using Typescript

export interface Cookies { Token: string; SessionID: string; UserID: string; } type property = keyof Cookies // property is "Token" | "SessionID" | "UserID" export const COOKIE_PROPERTIES: Record<property, property& ...

"Observables in RxJs: Climbing the Stairs of

Previously, I utilized Promise with async/await syntax in my Typescript code like this: const fooData = await AsyncFooData(); const barData = await AsyncBarData(); ... perform actions using fooData and barData However, when using RxJs Observable<T> ...

What steps should I take to resolve the Heroku TypeScript and Node.js build error I'm experiencing?

This is my first time using Heroku. I encountered multiple errors in the Heroku logs when trying to deploy my project: 2023-02-03T09:02:57.853394+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/" host=tech- ...

The issue of footer overlapping the login form is observed on iOS devices while using Safari and Chrome

Unique ImageI am currently working on an Angular 8 project with Angular Material. I have successfully designed a fully functional login page. However, I am encountering a problem specifically on iOS devices such as iPhones and iPads, whether it is Safari o ...