Creating a message factory in Typescript using generics

One scenario in my application requires me to define message structures using a simple TypeScript generic along with a basic message factory. Here is the solution I devised:

export type Message<
  T extends string,
  P extends Record<string, any> = Record<string, never>
> = {
  type: T;
  payload: P;
};

export const msg = <M extends Message<string, Record<string, any>>>(
  type: M['type'],
  payload: M['payload']
): M => ({ type, payload });

type UserRegistered = Message<'UserRegistered', { name: string }>;

const message = <UserRegistered>msg('UserRegistered', { name: 'Test' });

However, I encounter a TS2322 error on the ): M => ({ type, payload }); line:

error TS2322: Type '{ type: M["type"]; payload: M["payload"]; }' is not assignable to type 'M'. '{ type: M["type"]; payload: M["payload"]; }' can be assigned to the 'M' constraint, but 'M' could potentially be instantiated with a different subtype of constraint 'Message<string, Record<string, any>>'.

I am grappling with understanding why this poses a risk to type safety and why it fails to transpile, given that it essentially involves destructuring and reconstructing the same type.

(In case you are questioning the necessity of this thin factory layer, I believe it will enhance maintainability by providing a large set of predefined named factory functions)

Answer №1

When inferring function arguments, it's best to start from the bottom and work your way up instead of trying to infer the entire generic type all at once. Begin by inferring each nested property within the type before combining them together.

In many cases, you don't need to specify an explicit return type, especially when working with TypeScript's structural type system rather than a nominal one.

Avoid explicitly specifying generics when calling a function as TypeScript is usually able to infer all types automatically.

export type Message<
    T extends string,
    P extends Record<string, any> = Record<string, never>
    > = {
        type: T;
        payload: P;
    };

export const msg = <
    Type extends string,
    Payload extends Record<string, any> = Record<string, never>
>(
    type: Type,
    payload: Payload
): Message<Type, Payload> => ({ type, payload })

type UserRegistered = Message<'UserRegistered', { name: string }>;

const message = msg('UserRegistered', { name: 'Test' })

/**
 * UserRegistered is redundant here
 */
const message_: UserRegistered = msg('UserRegistered', { name: 'Test' })

Playground

For more details on inferring function arguments, check out my article 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

The attribute 'use' is not found within the data type 'typeof...', and the property 'extend' is not present within the data type 'typeof'

As I embark on building my very first Vue app using TypeScript, I find myself facing a frustrating issue: Property 'xxx' does not exist on type 'typeof. Despite my efforts to research similar problems, none of the suggested solutions have pr ...

Checking to see if a string meets the criteria of being a valid ARGB value

How do I verify that a string represents a valid ARGB value, such as #ffffffff for ARGB 255,255,255,255? Is there a way to validate this using TypeScript and C#? ...

Angular ReactiveForms not receiving real-time updates on dynamic values

I'm using reactive forms in Angular and I have a FormArray that retrieves all the values except for product_total. <tbody formArrayName="products"> <tr *ngFor="let phone of productForms.controls; let i=index" [formGroupName]="i"> ...

Applying Material UI class in React: Troubleshooting an error with your hook call

Recently, I have started using React and encountered an issue with a hook call. I understand the root cause of the problem but unsure how to resolve it without starting from scratch. Here is the snippet of the code: import { Fragment, PureComponent } from ...

Converting a unix timestamp to a Date in TypeScript - a comprehensive guide

To retrieve the unix timestamp of a Date in plain JavaScript and TypeScript, we can use this code snippet: let currentDate = new Date(); const unixTime = currentDate.valueOf(); Converting the unix timestamp back to a Date object in JavaScript is straight ...

How can we efficiently load paginated data from a database while still implementing pagination using Angular Material?

I have a large table with more than 1000 entries that I want to display using a <mat-table></mat-table>. Since loading all the entries at once would be too much, I am looking to implement pagination and load only 20 entries per page. The chal ...

Manually Enroll Node Module

Question: I am tackling a challenge in my TypeScript project where I need to interact with multiple APIs that are not available locally on my computer, but exist on the web. The code compiles without issues on my local machine as I have all the API declar ...

Exploring the integration of Styled-components in NextJs13 for server-side rendering

ERROR MESSAGE: The server encountered an error. The specific error message is: TypeError: createContext only works in Client Components. To resolve this issue, add the "use client" directive at the top of the file. More information can be found here i ...

DxDataGrid: Implementing a comprehensive validation system for multiple edit fields

I'm currently working with a DxDataGrid within an Angular Application. Within this particular application, I have the need to input four dates. I've implemented validation rules that work well for each individual field. However, my challenge aris ...

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 ...

Retrieving the input[text] value in TypeScript before trimming any special characters

One of the tasks I am working on involves a form where users can input text that may contain special characters such as \n, \t, and so on. My objective is to replace these special characters and then update the value of the input field accordingl ...

Definitions for nested directories within a main index.d.ts

We have developed custom typings for the latest version of material-ui@next and successfully included them in the library during the last beta release. For those interested, you can access the index.d.ts file here. However, there seems to be a problem wi ...

Is it possible to access the service and 'self' directly from the HTML template?

When working with Angular 6, one method to access component properties from a service is to pass 'self' to the service directly from the component. An example of this implementation is shown below: myComponent.ts public myButton; constructor(p ...

callback triggering state change

This particular member function is responsible for populating a folder_structure object with fabricated data asynchronously: fake(folders_: number, progress_callback_: (progress_: number) => void = (progress_: number) => null): Promise<boolean ...

What strategies can I implement to streamline the use of these functions instead of creating a separate one for each textfield

Currently, I am learning how to use spfx with SPO (SharePoint Online). In my form, there are multiple text fields that need to be handled. I have two class components named A and B. Whenever a textfield in component B is typed into, a function sends the in ...

Tips for Ensuring the Observable Completes Before Subscribing

I utilized RXJS operators in my code to retrieve an array of locations. Here is the code snippet: return O$ = this.db.list(`UserPlaces/${this.authData.auth.auth.currentUser.uid}`, { query: { orderByChild: 'deleted', equalTo: fal ...

Encountering compilation errors while using ng serve in NGCC

After attempting to update peer dependencies, I encountered an issue while compiling my Angular app. The error message displayed: Compiling @angular/material/core : es2015 as esm2015 Compiling @angular/material/expansion : es2015 as esm2015 Compiling @angu ...

How to iterate through the elements of an object within an array using Vue.js and TypeScript

There was an issue with rendering the form due to a TypeError: Cannot read properties of undefined (reading '0'). This error occurred at line 190 in the code for form1.vue. The error is also caught as a promise rejection. Error Occurred <inpu ...

Is it necessary for the version of the @types packages in TypeScript to match their non-types packages?

Are @types and untyped packages versioned the same way? npm i bluebird @types/bluebird -S returns "@types/bluebird": "^3.5.0", "bluebird": "^3.5.0", This seems logical. npm i request @types/request -S yields "@types/request": "0.0.41", "request": "^2. ...

There seems to be an issue with the useReducer value not updating when logging it in a handleSubmit function

I'm currently incorporating useReducer into my Login and Register form. Interestingly, when I attempt to log the reducer value, it only displays the default value. However, if I log it within the useEffect hook, it functions correctly. Below is a sn ...