The Event Typing System

I am currently in the process of setting up a typed event system and have encountered an issue that I need help with:

enum Event {
  ItemCreated = "item-created",
  UserUpdated = "user-updated",
}

export interface Events {
  [Event.ItemCreated]: (item: string) => void;
  [Event.UserUpdated]: (user: number) => void;
}

export interface EventListener<TEvent extends keyof Events = keyof Events> {
  event: TEvent;
  handler: (...args: Parameters<Events[TEvent]>) => void;
}

const UserListener: EventListener = {
  event: Event.UserUpdated,
  handler: (handlerArgs) => {
    console.log(handlerArgs);
  },
};

Currently, the type of handlerArgs is string | User, but I want it to be only User. What could be causing this discrepancy?

Answer №1

One issue with the

interface EventListener<TEvent extends keyof Events = keyof Events> {
  event: TEvent;
  handler: (...args: Parameters<Events[TEvent]>) => void;
}

lies in how, when you use EventListener without specifying a type argument, it defaults to the union type of keyof Events. As a result, both event and handler end up being linked to this union type independently.

The ideal scenario is for EventListener<TEvent> to distribute over unions within TEvent, so that EventListener<X | Y> translates to

EventListener<X> | EventListener<Y>
. This means EventListener (with no type argument) should itself be a union. Since interfaces cannot be unions, it has to be converted into a type alias.

There are different approaches to achieve this. One option is using a distributive conditional type to ensure a union input results in a union output:

type EventListener<TEvent extends keyof Events = keyof Events> = 
  TEvent extends any ? {
    event: TEvent;
    handler: (...args: Parameters<Events[TEvent]>) => void;
  } : never;

type E = EventListener
/* type E = {
    event: Event.ItemCreated;
    handler: (item: string) => void;
} | {
    event: Event.UserUpdated;
    handler: (user: number) => void;
} */

If there's no need to explicitly define the type argument, you can simplify by making EventListener a specific union type. This involves refactoring it as a distributive object type where you index into a mapped type to obtain the desired union:

type EventListener = { [TEvent in keyof Events]: {
  event: TEvent,
  handler: (...args: Parameters<Events[TEvent]>) => void
} }[keyof Events];

/* type EventListener = {
    event: Event.ItemCreated;
    handler: (item: string) => void;
} | {
    event: Event.UserUpdated;
    handler: (user: number) => void;
} */

By doing this, EventListener becomes a discriminated union, allowing the compiler to utilize event as a discriminant property to refine the handler property as needed:

const UserListener: EventListener = {
  event: Event.UserUpdated,
  handler: (handlerArgs) => {
    handlerArgs.toFixed(2); // valid
    console.log(handlerArgs);
  },
};

Playground 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

"Encountering a glitch in the Typescript/Node API where Express routes

Encountering a peculiar issue here - when attempting to import my router from an external file and add it as a route, I keep getting this unusual error where the string appears to be enclosed in double quotes. https://i.sstatic.net/nm9Wn.png ...

Angular TypeScript state management system

I am facing a challenge in connecting a controller to a state (using angular ui.router) where one way of writing it works, while the other does not. Successful example (with the controller registered under the module): this.$stateProvider .state(' ...

Tips for determining the overall percentage breakdown of 100% based on the individual denominator for every column within angular 8

In my code, I have a simple function that calculates the sum of numbers and strings in columns within a table. The sum calculation itself works fine and provides accurate results. However, the problem arises when I attempt to divide the total sum of each c ...

Error in TypeScript when utilizing generic callbacks for varying event types

I'm currently working on developing a generic event handler that allows me to specify the event key, such as "pointermove", and have typescript automatically infer the event type, in this case PointerEvent. However, I am encountering an error when try ...

Alert displaying NextJS props

I recently began learning Next.js and have encountered an issue while trying to pass props from a parent component to a child component. The error message I'm seeing is: Type '({ name }: { name: any; }) => JSX.Element' is not assignable ...

What are some solutions to the "t provider not found" error?

Upon deploying my application on the production server using GitLab/Docker, I encountered the following error message: ERROR Error: No provider for t! at C (vendor.32b03a44e7dc21762830.bundle.js:1) at k (vendor.32b03a44e7dc21762830.bundle.js:1) at t._thr ...

Incorporating a CSS Module into a conditional statement

Consider the following HTML structure <div className={ `${style.cell} ${cell === Player.Black ? "black" : cell === Player.White ? "white" : ""}`} key={colIndex}/> Along with the associated CSS styles .cell { ...

In the context of NextJs, the req.body is treated as an object within the middleware, but transforms

Here is the middleware function responsible for handling the origin and CORS: export async function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers) const origin = requestHeaders.get('origin') ?? '& ...

"Error: The specified object does not have the capability to support the property or method 'includes'." -[object Error]

Recently, I developed a method that utilizes both indexOf() and includes(). However, I encountered an error message stating "Object doesn't support property or method 'includes'". I have attempted to run the method on both Internet Explorer ...

Setting IDPs to an "enabled" state programmatically with AWS CDK is a powerful feature that allows for seamless management of

I have successfully set up Facebook and Google IDPs in my User Pool, but they remain in a 'disabled' state after running CDK deploy. I have to manually go into the UI and click on enabled for them to work as expected. How can I programmatically e ...

The unit test is not passing due to inconsistencies between the mock data generated in the constructors and the original mock data

Currently, I am delving into the world of unit testing and have created a test to work on. Here is what I have so far: const EXEPECTED: MergedFood = { id: '1', name: 'test mergedFood', ingredients: { '2': ...

Avoid triggering the onClick event on specific elements in React by utilizing event delegation or conditional rendering

programming environment react.js typescript next.js How can I prevent the onClick process from being triggered when the span tag is pressed? What is the best approach? return ( <div className="padding-16 flex gap-5 flex-container" ...

Converting a String variable to a String Literal Type in Typescript: A step-by-step guide

When working with Typescript, imagine I need to call a function that has the following signature- function foo(param: "TRUE"|"FALSE"|"NONE") Is there a way to achieve something like this- var str = runtimeString() if(str === "TRUE" | str === "FALSE" | s ...

Accessing an Array from a service method in Angular and passing it to my main component

Within my api-service.ts, I have an array that holds some data. public getData():Observable<any[]> { return Observable.toString[ObsResult]; } Now, in the main component, I am attempting to call the getData() method to render the data in ...

Guide on importing CDN Vue into a vanilla Typescript file without using Vue CLI?

In the midst of a large project that is mostly developed, I find myself needing to integrate Vue.js for specific small sections of the application. To achieve this, I have opted to import Vue.js using a CDN version and a <script> </script> tag ...

How can I convert the date format from ngbDatepicker to a string in the onSubmit() function of a form

I'm facing an issue with converting the date format from ngbDatepicker to a string before sending the data to my backend API. The API only accepts dates in string format, so I attempted to convert it using submittedData.MaturityDate.toString(); and su ...

What is the best way to reset a dropdown list value in angular?

Is there a way to erase the selected value from an Angular dropdown list using either an x button or a clear button? Thank you. Code <div fxFlex fxLayout="row" formGroupName="people"> <mat-form-field appearance=&quo ...

Struggling to incorporate generics into a Typescript method without sacrificing the typing of object keys

Currently, I am working on a method in Typescript that is responsible for extracting allowable property types from an object of a constrained generic type. The scenario involves a type called ParticipantBase which consists of properties like first: string ...

Retrieve the data from the mat-checkbox

My goal is to retrieve a value from a mat-checkbox, but the issue is that we only get boolean expression instead of the string value. Here's an example snippet of what I'm looking for: <mat-checkbox formControlName="cb2" <strong&g ...

`The process of converting Typescript to ES5 through compiling/transpiling is encountering issues`

My current project involves using Webpack and integrating angular2 into the mix. To achieve this, I made adjustments to my setup in order to compile TypeScript. Following a resource I found here, my plan was to first compile TypeScript to ES6 and then tra ...