Type for handling event unions

My goal is to implement a type for an event handler that enables autocomplete functionality for event data. The events I need to handle have the following structure:

type MyEvent =
  | {
    eventA: {
      foo: number;
      bar: number;
    };
  }
  | {
    eventB: {
      baz: string;
    };
  };

Here's what I've managed to come up with for the handler so far:

type UnionKeys<T> = T extends T ? keyof T : never;

type Handler = {
  [K in UnionKeys<MyEvent>]: (data: /* ??? */) => void;
};

const handler: Handler = {
  eventA: (data) => console.log(data.foo), // The aim is to have functional autocomplete for data.* at this point
  eventB: (data) => console.log(data.baz),
};

I'm struggling to define a type for 'data' within the Handler type to properly correspond to the event. Any suggestions?

Answer №1

If you wish to maintain the exact structure of MyEvent, take a look at this example:

type UnionKeys<T> = T extends T ? keyof T : never;

// credits to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnionHelper<T, TAll> =
    T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

type StrictUnion<T> = StrictUnionHelper<T, T>

type MyEvent = StrictUnion<
    | {
        eventA: {
            foo: number;
            bar: number;
        };
    }
    | {
        eventB: {
            baz: string;
        };
    }>;

type Handler = {
    [K in keyof MyEvent]: (data: NonNullable<MyEvent[K]>) => void;
};

const handler: Handler = {
    eventA: (data) => {
        data.bar // ok
        data.foo // ok
    },
    eventB: (data) => {
        data.baz // ok
    }
};

Playground

However, I don't like that I was compelled to use NonNullable. It appears to be somewhat of a workaround. You can actually extract all keys using distributive conditional types as shown below:

type MyEvent =
    | {
        eventA: {
            foo: number;
            bar: number;
        };
    }
    | {
        eventB: {
            baz: string;
        };
    };

type Keys<T> = T extends Record<infer Key, infer _> ? Key : never;

type Values<T> = T[keyof T]

type Handler = {
    [K in Keys<MyEvent>]: (data: Values<Extract<MyEvent, Record<K, unknown>>>) => void;
};

const handler: Handler = {
    eventA: (data) => {

        data.bar // ok
        data.foo // ok
    },
    eventB: (data) => {
        data.baz // ok
    }
};

Playground

Please bear in mind that there is a simpler approach available. It might be worth considering creating a mapped data structure, similar to the one mentioned here:

type MyEvent = {
    eventA: {
        foo: number;
        bar: number;
    },
    eventB: {
        baz: string;
    }
}

type Handler = {
    [K in keyof MyEvent]: (data: MyEvent[K]) => void;
};

const handler: Handler = {
    eventA: (data) => {
        data.bar // ok
        data.foo // ok
    },
    eventB: (data) => {
        data.baz // ok
    }
};

You may find this example interesting as well

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

Why isn't the parent (click) event triggered by the child element in Angular 4?

One of my challenges involves implementing a dropdown function that should be activated with a click on this specific div <div (click)="toggleDropdown($event)" data-id="userDropdown"> Username <i class="mdi mdi-chevron-down"></i> </d ...

What causes Gun.js to generate duplicate messages within a ReactJs environment?

I need assistance with my React application where gun.js is implemented. The issue I am facing is that messages are being duplicated on every render and update. Can someone please review my code and help me figure out what's wrong? Here is the code s ...

What is the most effective method to create a versatile function in Angular that can modify the values of numerous ngModel bindings simultaneously?

After working with Angular for a few weeks, I came across a problem that has me stumped. On a page, I have a list of about 100 button inputs, each representing a different value in my database. I've linked these inputs to models as shown in this snipp ...

Local machine encountering Typescript error, while remote test server remains unaffected

I am currently exploring the Microsoft Fabric UI tools and encountering an error on my local machine when trying to use them. /Developer/React/TCV.ts/tcv/src/CategorySelection.tsx(94,9): Type '(filterText: string, currentPersonas: IPersonaProps[], lim ...

Issue: The JSX element 'X' is missing any constructors or call signatures

While working on rendering data using a context provider, I encountered an error message stating "JSX Element type Context does not have any constructor or call signatures." This is the code in my App.tsx file import { Context } from './interfaces/c ...

Submitting a form via NextJS to an internal API

After reading through the Next.JS documentation, I came across an interesting point. Note: Instead of using fetch() to call an API route in getStaticProps, it's recommended to directly import the logic from within your API route and make necessary cod ...

Unveiling the secrets of the Google Region Lookup API

I am struggling to incorporate the Region Area Lookup feature from Google Maps into my project. Despite it being an experimental feature, I am having difficulty getting it to function correctly. Initially, I attempted to integrate this feature into a Reac ...

Employ material-ui default prop conditionally

I am implementing a StepLabel component in material ui. Depending on the props passed to the parent, I may need to make changes to the icon property of the StepLabel: interface Props { customClasses?: ClassNameMap; customIcon?: ReactNode; } const MySt ...

Jasmine's await function often leads to variables remaining undefined

When testing a function, I encountered an issue where a variable is created and assigned a value, but the payload constant always remains undefined. campaigns-card.component.ts async ngOnInit() { const { uid } = await this.auth.currentUser const { ...

Testing vue-router's useRoute() function in Jest tests on Vue 3

Struggling with creating unit tests using Jest for Vue 3 components that utilize useRoute()? Take a look at the code snippet below: <template> <div :class="{ 'grey-background': !isHomeView }" /> </template> &l ...

Make the text stand out by highlighting it within a div using a striking blue

Currently, I am working with Angular2 and have incorporated a div element to display multiple lines of text. Positioned below the text is a button that, when clicked, should select the entirety of the text within the div (similar to selecting text manually ...

Delivering secure route information to paths in Angular 2

When defining my routes in the routing module, I have structured the data passing like this: const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'login', component: LoginCompon ...

Common mistakes made while working with decorators in Visual Studio Code

Having trouble compiling TypeScript to JavaScript when using decorators. A persistent error message I encounter is: app.ts:11:7 - error TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the ' ...

PrimeNG Multiselect: Simplifying selection of entire groups

I am currently working with a PrimeNG grouped multi select component and I'm curious if there is a way to select all items within a group by simply clicking on the group itself? Below is the code that I have implemented so far: <p-multiSelect [opt ...

Unpacking the information in React

My goal is to destructure coinsData so I can access the id globally and iterate through the data elsewhere. However, I am facing an issue with TypeScript on exporting CoinProvider: Type '({ children }: { children?: ReactNode; }) => void' is no ...

Transform array sequences into their own unique sequences

Reorder Array of List to Fit My Custom Order Current Output: [ { "key": "DG Power Output", "value": "6.00", "unit": "kWh", }, { "key": "DG Run Time", "value": "5999999952", "unit": "minutes", }, { "key": "Fuel Level (Before)", "value": "8.00" ...

IntelliJ is indicating a typescript error related to react-bootstrap-table-next

Working with react-bootstrap-table-next (also known as react-bootstrap-table2) has been causing a Typescript error in my IntelliJ environment, specifically on the validator field within my column definition. Despite trying various solutions, such as adding ...

Angular 12: An issue has occurred due to a TypeError where properties of undefined cannot be read, specifically pertaining to the element's 'nativeElement

Within my phone number input field, I have integrated a prefixes dropdown. Below is the code snippet for this feature. HTML code in modal <div class="phone" [ngClass]="{ 'error_border': submitted && f.phoneNumber.er ...

Executing callback in the incorrect context

I'm facing an issue and can't seem to navigate through it. I am setting up a callback from a third party directive, but the callback is not returning with the correct scope. This means that when it reaches my controller, this refers to some other ...

Unlocking Not Exported Type Definitions in TypeScript

Take a look at this TypeScript code snippet: lib.ts interface Person { name: string; age: number; } export default class PersonFactory { getPerson(): Person { return { name: "Alice", age: 30, } } } ...