Create a well-designed function that assigns events to handlers in a generic way

Here we are continuing from the previous exploration that aims to develop a reusable mechanism for assigning incoming events (messages) to appropriate event handlers while maintaining complete type reliance. Our goal is to create a reusable function for handling events.

const handleEvent = 
  <EventKind extends keyof EventsMap>
  (e: Event<EventKind>): Promise<void> => {
  const kind: EventKind = e.kind;
  const handler = <(e: CrmEvent<EventKind>) => Promise<void>>handlers[kind]; // The unnecessary assertion here prompts us to make this function generic.
  return handler(e);
};

Our desired end result is:

const handleEvent = eventAssigner<CrmEventsMap>(handlers, 'kind');

It all starts with a map connecting event discriminators to event details:

interface CrmEventsMap {
  event1: { attr1: string,  attr2: number }
  event2: { attr3: boolean, attr4: string }
}

This allows us to define the complete Event type, including discriminator:

type CrmEvent<K extends keyof CrmEventsMap> = { kind: K } & EventsMap[K]

We now have everything necessary to create the handlers map:

const handlers: { [K in keyof CrmEventsMap]: (e: CrmEvent<K>) => Promise<void> } = {
  event1: ({attr1, attr2}) => Promise.resolve(),
  event2: ({attr3, attr4}) => Promise.resolve(),
};

This leads us back to handleEvent. The need for a type assertion within the function body is a strong motivator to make it generic.

Let's try to simplify with this approach:

const eventAssigner =
  <EventMap extends {},
    EventKind extends keyof EventMap,
    KindField extends string>
  (
    handlers: { [k in keyof EventMap]: (e: EventType<EventMap, k, KindField>) => any },
    kindField: KindField
  ) =>
    (e: EventType<EventMap, EventKind, KindField>):
      ReturnType<(typeof handlers)[EventKind]> => {
      const kind = e[kindField];
      const handler = <(e: EventType<EventMap, EventKind, KindField>) => ReturnType<(typeof handlers)[EventKind]>>handlers[kind];
      return handler(e);
    };

type EventType<EventMap extends {}, Kind extends keyof EventMap, KindField extends string> =
  { [k in KindField]: Kind } & EventMap[Kind]

Although complex, the usage of this function is still manageable. By fixing the event discriminator field to 'kind', we greatly simplify things:

const eventAssigner =
  <EventMap extends {},
    EventKind extends keyof EventMap>
  (handlers: { [k in keyof EventMap]: (e: EventType<EventMap, k>) => any }) =>
    (e: EventType<EventMap, EventKind>):
      ReturnType<(typeof handlers)[EventKind]> =>
      handlers[e.kind](e);

type EventType<EventMap extends {}, Kind extends keyof EventMap> = { kind: Kind } & EventMap[Kind]

What's intriguing is that this version does not require a type assertion, unlike the previous one.

To use either of these functions, concrete type arguments must be provided by wrapping them in another function:

const handleEvent = 
  <E extends CrmEventKind>
  (e: CrmEvent<E>): ReturnType<(typeof handlers)[E]> => 
    eventAssigner<CrmEventMap, E>(handlers)(e);

In conclusion, how close do you think we can get to achieving the ideal implementation?

Explore in TypeScript Playground

Answer №1

Once I took a moment to process the information, a clearer picture emerged.

To start, I recommend loosening the constraints on your handlers variable regarding handler arguments needing the "kind" discriminant:

interface CrmEventMap {
  event1: { attr1: string; attr2: number };
  event2: { attr3: boolean; attr4: string };
}

const handlers: {
  [K in keyof CrmEventMap]: (e: CrmEventMap[K]) => Promise<void>
} = {
  event1: ({ attr1, attr2 }) => Promise.resolve(),
  event2: ({ attr3, attr4 }) => Promise.resolve()
};

Therefore, there's no need for CrmEvent<K> in this case. Despite that, your eventual handleEvent implementation will utilize a discriminant to dispatch events appropriately, but the current setup of handlers doesn't necessitate it: each function only handles events that have already been dispatched correctly. You can retain the structure as is if needed, but it seems excessive to me.

Now, let's examine the eventAssigner implementation:

const eventAssigner = <
  M extends Record<keyof M, (e: any) => any>,
  D extends keyof any
>(
  handlers: M,
  discriminant: D
) => <K extends keyof M>(
  event: Record<D, K> & (Parameters<M[K]>[0])
): ReturnType<M[K]> => handlers[event[discriminant]](event);

Therefore, eventAssigner entails a curried generic function. It accepts generics M, representing the type of handlers object (as referenced by the handlers variable), which must hold one-argument function properties, and D, signifying the type of discriminant (the value currently set as "kind") which should be a valid key type. The function then returns another function that is generic in K, intended to correspond to one of the keys within M. Its event parameter is defined as

Record<D, K> & (Parameters<M[K]>[0])
, stipulating that it must mirror the same type argument as the property keyed by K in M, alongside an object with a discriminant bearing key D and value K; this mirrors your CrmEvent<K> type definition.

The function ends by returning ReturnType<M[K]>. The absence of a type assertion here is due to the constraint on

M</code, where each handler function extends <code>(e: any)=>any
. Consequently, when the compiler analyzes handlers[event[discriminant]], a function is identified that must align with (e: any)=>any; therefore, you could essentially call it using any argument and return any type. Nevertheless, exercising caution is advised. Instead of using any, an alternative like (e: never)=>unknown could enhance safety, albeit necessitating a type assertion. The choice rests with you.

Subsequently, here’s how you put it into practice:

const handleEvent = eventAssigner(handlers, "kind");

Note that you're relying on generic type inference here, eliminating the need for explicit specifications such as <CrmEventsMap>. In my view, allowing type inference is superior to manual specification. If you did wish to specify something, it would resemble

eventAssigner<typeof handlers, "kind">(handlers, "kind")
, which appears unnecessary.

Confirming that it functions as expected:

const event1Response = handleEvent({ kind: "event1", attr1: "a", attr2: 3 }); // Promise<void>
const event2Response = handleEvent({ kind: "event2", attr3: true, attr4: "b" }); // Promise<void>

All seems well. Best of luck as you move forward!

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

I'm experiencing some difficulties utilizing the return value from a function in Typescript

I am looking for a way to iterate through an array to check if a node has child nodes and whether it is compatible with the user's role. My initial idea was to use "for (let entry of someArray)" to access each node value in the array. However, the "s ...

What sets apart the commands npm install --force and npm install --legacy-peer-deps from each other?

I'm encountering an issue while trying to set up node_modules for a project using npm install. Unfortunately, the process is failing. Error Log: npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolv ...

When using setInterval, the image remains static on Safari but updates on Chrome

In my project, I am using a mock array to distribute data. One part of the project utilizes this data to display a list of cases, each with assigned images. When a case is hovered over, the images associated with that case are displayed one at a time, with ...

Autoplay halts on Ionic 3 image slider following manual slide navigation

My Ionic 3 image slider has autoplay functionality that works perfectly. However, I've noticed that when I manually slide the images, the autoplay feature stops working. I have included my Ionic 3 code below. I would greatly appreciate any help on thi ...

Receiving "this" as an undefined value within the TypeScript class

Currently developing a rest api using express.js, typescript, and typeorm. Initially thought the issue was with AppDataSource, but it seems to be functioning properly. Below is my controller class: import { RequestWithBody } from '@utils/types/reque ...

What classification should be given to children when they consist solely of React components?

I'm encountering an issue where I need to access children's props in react using typescript. Every time I attempt to do so, I am faced with the following error message: Property 'props' does not exist on type 'string | number | boo ...

Begin the NextJS project by redirecting the user to the Auth0 page without delay

I am new to coding and currently working on a project using Typescript/NextJS with Auth0 integration. The current setup navigates users to a page with a login button that redirects them to the Auth0 authentication page. However, this extra step is unneces ...

Tips for saving true or false values in a dynamic form?

Is there a way to store boolean values in a reactive form where a button can have the value of true or false? The goal is to be able to access the inputs of these buttons. However, I am facing an issue because there is another form on this page for text in ...

Tips for getting Nativescript listview to function properly

I am currently developing an app using nativescript and angular 2. I am facing some issues while trying to implement the nativescript listview component. Whenever I run the app, all I see is " [object object] ". Below is my view code : <grid-layout c ...

Retrieving the final element from a TypeScript JSON array

I am trying to retrieve the value of the "id" property from the last element in an array of JSON objects. While I can easily find each element by id, I specifically need to extract the value of the last "id" in the array of JSON objects. In the example p ...

Weird occurrences in Typescript generics

function resizeImage<T extends File | Blob>(input: T, width: number, height: number): Promise<T> { return Promise.resolve(new File([new Blob()], 'test.jpg')) } Error: (48, 3) TS2322:Type 'Promise' is not assignable to ...

Using Typescript and React to render `<span>Text</span>` will only display the text content and not the actual HTML element

My function is a simple one that splits a string and places it inside a styled span in the middle. Here's how it works: splitAndApplyStyledContent(content: string, textType: string, separator: string) { const splittedContent = content.split(separat ...

Exploring the versatility of Angular/Ionic Storage for dynamic data storage

Recently, I encountered an issue with my code where the data selected by the user gets parsed into an array, but there is no way to store this data permanently. As the user navigates away from the component, the data vanishes. I attempted to utilize Ionic ...

Ways to access configuration settings from a config.ts file during program execution

The contents of my config.ts file are shown below: import someConfig from './someConfigModel'; const config = { token: process.env.API_TOKEN, projectId: 'sample', buildId: process.env.BUILD_ID, }; export default config as someCo ...

Manufacturing TypeScript classes that are returned by a factory

Developed a custom library that generates classes based on input data and integrates them into a main class. To enhance code maintainability and readability, the logic for generating classes has been extracted into a separate file that exports a factory f ...

ng namespace not found

I am currently in the process of converting a simple Angular 1.6 project to TypeScript. I have declared all the necessary typings dependencies and they have been compiled and installed successfully. However, I am encountering a compilation error stating "C ...

Ways to alter an array of objects without using a loop

The following code is functioning properly: for(let i=0;i< this.Array.length ; i++){ if(this.Array[i].propertyObject.hasOwnProperty('header')) this.Array[i].ColumnName = this.Array[i].propertyObject.header; } I am int ...

I am hoping to refresh my data every three seconds without relying on the react-apollo refetch function

I am currently working with React Apollo. I have a progress bar component and I need to update the user's percent value every 3 seconds without relying on Apollo's refetch method. import {useInterval} from 'beautiful-react-hooks'; cons ...

Run a script in a newly opened tab using the chrome.tabs.create() method

Struggling with executing a script using chrome.tabs.executeScript() in the tab created with chrome.tabs.create()? Despite searching for solutions, nothing seems to be working as expected. Check out my current code below: runContentScript(){ c ...

Angular - enabling scroll position restoration for a singular route

Is there a way to turn off scroll restoration on a specific page? Let's say I have the following routes in my app-routing.module.ts file... const appRoutes: Routes = [{ path: 'home', component: myComponent}, { path: 'about', compon ...