Creating a mapping for a dynamic array of generic types while preserving the connection between their values

In my code, I have a factory function that generates a custom on() event listener tailored to specific event types allowed for user listening. These events are defined with types, each containing an eventName and data (which is the emitted event data). My goal is to link these two elements so that when listening for a particular event, the corresponding data will be accessible in the handler.

Here is an example of the implementation:

interface DefaultEventBody {
  eventName: string
  data: unknown
}

interface ChristmasEvent extends DefaultEventBody {
  eventName: 'christmas',
  data: {
    numberOfPresentsGiven: number
  }
}

interface EasterEvent extends DefaultEventBody {
  eventName: 'easter',
  data: {
    numberOfEggsGiven: number
  }
}

export function Listener <EventBody extends DefaultEventBody> () {

  return (eventName: EventBody['eventName'], fn: (data: EventBody['data']) => void) => {
    // someEmitter.on(eventName, fn)
  }
}

const spoiltBrat = { on: Listener<EasterEvent|ChristmasEvent>() }

spoiltBrat.on('christmas', (data) => {

  console.log(data.numberOfPresentsGiven)

})

TypeScript correctly recognizes that the passed eventName can be either christmas or easter, but it struggles to infer the type of data in the handler function, resulting in an error when attempting to access data.numberOfPresentsGiven.

Error: Property 'numberOfPresentsGiven' does not exist on type '{ numberOfPresentsGiven: number; } | { numberOfEggsGiven: number; }'. Property 'numberOfPresentsGiven' does not exist on type '{ numberOfEggsGiven: number; }'.(2339)

I understand why this issue occurs, as neither ChristmasEvent nor EasterEvent contain identical numberOf* properties. However, I am curious if there is a solution to achieve the desired functionality?

Update

Upon request from Captain Yossarian, here is a Playground Link showcasing the nearly finished script.

Answer №1

Quick fix

Check out this solution:

type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
  T extends any
  ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;

// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnion<T> = StrictUnionHelper<T, T>

export function Listener<EventBody extends DefaultEventBody>() {

  return (eventName: EventBody['eventName'], fn: (data: StrictUnion<EventBody['data']> /** <---- change is here */) => void) => {
    // someEmitter.on(eventName, fn)
  }
}


const spoiltBrat = {
  on: Listener<EasterEvent | ChristmasEvent>()
}

spoiltBrat.on('christmas', (data) => {
  data.numberOfPresentsGiven // <---- DRAWBACK, number | undefined
})

The above approach works but has limitations. Specifically, the issue with allowing numberOfPresentsGiven to be potentially undefined.

Extended solution

If you are aiming for a robust publish/subscribe logic setup, consider using overloadings as shown below:

type AllowedEvents = ChristmasEvent | EasterEvent

type Events = AllowedEvents['eventName']

type Overloadings = {
  [Prop in Events]: (eventName: Prop, fn: (data: Extract<AllowedEvents, { eventName: Prop }>) => void) => void
}

...

To delve deeper into how to implement this type of functionality and apply it effectively to your codebase, read on.

...

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 functionality for handling gestures on AgmMap appears to be non-functional

I am currently using the AGM Map feature available on and I need to disable the zooming functionality when scrolling. Despite setting gestureHandling = "'cooperative'", it does not seem to work. Are there any specific factors causing this issue? ...

What causes the distinction between entities when accessing objects through TestingModule.get() and EntityManager in NestJS?

Issue: When using TestingModule.get() in NestJS, why do objects retrieved from getEntityManagerToken() and getRepositoryToken() refer to different entities? Explanation: The object obtained with getEntityManagerToken() represents an unmocked EntityManag ...

"Encountered a 'NextAuth expression cannot be called' error

Recently, I delved into learning about authentication in Next.js using next-auth. Following the documentation diligently, I ended up with my app/api/auth/[...nextauth]/route.ts code snippet below: import NextAuth, { type NextAuthOptions } from "next-a ...

The type 'Store<unknown, AnyAction>' is lacking the properties: dispatch, getState, subscribe, and replaceReducer

I have configured the redux store in a public library as follows: import { configureStore } from '@reduxjs/toolkit'; import rootReducer from '@/common/combineReducer'; import { createLogger } from 'redux-logger'; import thunk ...

Generate a blueprint for a TypeScript interface

In my coding project, I've been noticing a pattern of redundancy when it comes to creating TypeScript interfaces as the code base expands. For example: interface IErrorResponse { code: number message: string } // Feature 1 type FEATURE_1_KEYS = ...

In a Custom Next.js App component, React props do not cascade down

I recently developed a custom next.js App component as a class with the purpose of overriding the componentDidMount function to initialize Google Analytics. class MyApp extends App { async componentDidMount(): Promise<void> { await initia ...

Issue encountered while declaring a variable as a function in TSX

Being new to TS, I encountered an interesting issue. The first code snippet worked without any errors: interface Props { active: boolean error: any // unknown input: any // unknown onActivate: Function onKeyUp: Function onSelect: Function onU ...

Having trouble implementing the ternary operator (?) in TypeScript within a Next.js project

Trying to implement a ternary operator condition within my onClick event in a Next.js application. However, encountering an error indicating that the condition is not returning any value. Seeking assistance with resolving this issue... https://i.stack.img ...

What could be causing the React text input to constantly lose focus with every keystroke?

In my React project using Material-UI library, I have a component called GuestSignup with various input fields. const GuestSignup = (props: GuestSignupProps) => { // Component code goes here } The component receives input props defined by an ...

A Vue object with dynamic reactivity that holds an array of objects

I've experimented with various approaches, but so far I've only managed to get this code working: // This works <script setup lang="ts"> import { reactive } from 'vue' import { IPixabayItem } from '../interfaces/IPi ...

Is the return type determined by the parameter type?

I need to create an interface that can handle different types of parameters from a third-party library, which will determine the return type. The return types could also be complex types or basic types like void or null. Here is a simple example demonstra ...

Enhancing React with TypeScript: Best Practices for Handling Context Default Values

As I dive into learning React, TypeScript, and Context / Hooks, I have decided to create a simple Todo app to practice. However, I'm finding the process of setting up the context to be quite tedious. For instance, every time I need to make a change t ...

Guide on converting a JSON object into a TypeScript Object

I'm currently having issues converting my JSON Object into a TypeScript class with matching attributes. Can someone help me identify what I'm doing wrong? Employee Class export class Employee{ firstname: string; lastname: string; bi ...

Exploring the use of Jest for testing delete actions with Redux

I've been working on testing my React + Redux application, specifically trying to figure out how to test my reducer that removes an object from the global state with a click. Here's the code for my reducer: const PeopleReducer = (state:any = init ...

What is the process for transforming a string literal type into the keys of a different type?

Imagine having a string literal type like this: type Letters = "a" | "b" | "c" | "d" | "e"; Is there a way to create the following type based on Letters? type LetterFlags = {a: boolean, b: boolean, c: bool ...

Exploring the inner components of an entity without the need for external tools

I am currently enhancing TypeScript usage in a project by implementing generics. The challenge I am facing involves dealing with a complex object retrieved from the backend, which consists of a class with numerous attributes, most of which are classes them ...

Surprising Media Component Found in URL Parameters within the design

Exploring the page structure of my Next.js project: events/[eventId] Within the events directory, I have a layout that is shared between both the main events page and the individual event pages(events/[eventId]). The layout includes a simple video backgro ...

What are the differences between an optional property and a non-optional property?

Let's say I am working on creating an array of type CoolObject. What would be the better approach if some objects have the property format, while others do not? // Option 1 export interface CoolObject { name: string; color: string; ...

Utilizing Visual Studio Code for setting breakpoints in Typescript Jasmine tests

Currently, I am in the process of configuring a launch setup in Visual Studio Code for debugging my unit tests. The unit tests are written in Typescript, and both the tests and the corresponding code are compiled into a single js file with a source map. ...

Unexpected behavior observed with Angular Toggle Button functionality

Having trouble implementing toggle functionality in Angular where different text is displayed when a button is toggled. I keep getting an error in my code, can anyone assist? See the code below: HTML <tr> <td>Otto</td> <td> ...