Utilize a dynamically defined union type to create a versatile callback function

I'm currently working on creating a message subscription function. A basic version without types is shown below:

function createMessage(message) {
  postMessage(message)
}

function addSubscriber(messageType, callback) {
  handleNewMessage(message => {
    if (message.type === messageType) {
       callback(message.data)
    }
  })
}

The createMessage function can support multiple message types. To achieve this, I define each type separately and then use a discriminated union to type the message argument. Example:

type InfoMessage = {
  type: 'info', 
  data: {
    x: string,
    y: number
  }
}

type ErrorMessage = {
  type: 'error'
}

type MessageType = InfoMessage | ErrorMessage

Notice how ErrorMessage does not have any associated data.

This allows me to type the createMessage function as follows:

  function createMessage(message:MessageType):void {
    postMessage(message)
  }

However, I am having trouble figuring out the correct typing for the addSubscriber function. Here's what I have so far:

function addSubscriber(messageType: MessageType['type'], callback) {
  handleNewMessage(message => {
    if (message.type === messageType) {
       callback(message.data)
    }
  })
}

I am uncertain about how to properly type the callback. Using MessageType['data'] generates an error since data may not always be present. Even adding data:undefined to ErrorMessage results in losing the connection between the message type and its data.

My ideal scenario would involve writing

addSubscriber('error', (data) => console.log(data))
with TypeScript recognizing that data is actually undefined in this context.

One approach I've considered is declaring the function type individually for each message type, but this seems cumbersome as it requires defining both the MessageType and the corresponding subscribe function for each type of message.

In practice, I have numerous messages and would prefer to avoid this repetitive process.

What would be the most effective way to type the addSubscriber's callback function?

Answer №1

Below are some techniques you can utilize:

  • Introduce a generic type parameter T to aid TypeScript in narrowing the message type when statically known
  • Employ a distributive conditional type with Extract, enabling narrowing to a specific member type of a distributed union. Refer to this Stack Overflow thread for more insights
  • Use a conditional type to extract the type of data from a message only if it's present as a key
type TypedMessage<T extends MessageType['type']> = Extract<MessageType, { type: T }>;

type MessageData<M extends MessageType> = M extends { data: any } ? M['data'] : undefined;

function subscribe<T extends MessageType['type']>(
  messageType: T,
  callback: (data: MessageData<TypedMessage<T>>) => void
) {
  handleNewMessage(message => {
    if (message.type === messageType) {
       callback((message as { data?: any }).data)
    }
  })
}

subscribe('foo', (data) => console.log(data)); // typeof data === FooMessage['data']
subscribe('bar', (data) => console.log(data)); // typeof data === undefined

Explore further on this topic at the TypeScript Playground through this link 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

Different ways to categorize elements of Timeline using typescript

I have some code that generates a timeline view of different stages and their corresponding steps based on an array of stages. Each stage includes its name, step, and status. My goal is to organize these stages by name and then display the steps grouped un ...

Ways to store a component in cache once its route is triggered

There are 3 components in my project: 1 parent and 2 child components with router outlet. The child component becomes active whenever its route is called, sharing data using a service. Both of these child components have complex views. When switching bet ...

Dismiss the necessity of imports in Angular

I am facing an issue with using a library in Angular, specifically the cubing npm package. This library is designed to run in both the browser and node environments, with specific code for each. I want it to run in the browser, but when compiling with Angu ...

Returns false: CanActivate Observable detects a delay during service validation

Issue with Route Guard in Angular Application: I encountered an issue with my route guard in my Angular application. The problem arises when the guard is active and runs a check by calling a service to retrieve a value. This value is then mapped to true or ...

Issue with Angular ngFor not updating radio button value when ngModel is set

Hello, I am fairly new to working with Angular and could really use some assistance with a problem I've run into. Essentially, I am receiving an array of objects from an API like this: [{name: "abc", score: 2},{name: ""def, score: ...

What is the reason for TypeScript not providing warnings for unrealistic conditions involving 'typeof' and 'in'?

The recent updates in version 4.9 highlighted the enhanced narrowing with 'in'. Intrigued by this, I decided to experiment with their example in a coding playground. Surprisingly, I discovered that seemingly impossible conditions involving typeof ...

What is the correct version compatibility matrix for Expo, NPM, Node, React Native, and TypeScript?

Currently, I am in the process of setting up React Native with TypeScript. Here are the steps I followed: npx react-native init MyApp --template react-native-template-typescript I made sure to install TypeScript as well: npm install -g typescript ' ...

What is the correct way to implement Vue.use() with TypeScript?

I am trying to incorporate the Vuetify plugin into my TypeScript project. The documentation (available at this link) suggests using Vue.use(), but in TypeScript, I encounter the following error: "error TS2345: Argument of type '{}' is not assign ...

Troubleshooting issues with importing modules in TypeScript when implementing Redux reducers

Struggling to incorporate Redux with TypeScript and persist state data in local storage. My current code isn't saving the state properly, and as I am still new to TypeScript, I could really use some suggestions from experienced developers. Reducers i ...

Guide on formatting the API response using a callback function in Angular development

How can I reformat my API response using a callback function and access the data within the angular subscribe method? I attempted to use mergemap but it didn't work as expected. this.http.get('https://some.com/questions.xml', {headers, res ...

Adjusting the value of a mat-option depending on a condition in *ngIf

When working with my mat-option, I have two different sets of values to choose from: tempTime: TempOptions[] = [ { value: 100, viewValue: '100 points' }, { value: 200, viewValue: '200 points' } ]; tempTimesHighNumber: TempOpt ...

TypeScript does not recognize the $.ajax function

Looking for help with this code snippet: $.ajax({ url: modal.href, dataType: 'json', type: 'POST', data: modal.$form.serializeArray() }) .done(onSubmitDone) .fail(onSubmitFail); ...

Is it possible to assign an alternative name for the 'require' function in JavaScript?

To ensure our node module is executable and includes dependencies for requiring modules at runtime, we utilize the following syntax: const cust_namespace = <bin>_require('custom-namespace'); This allows our runtime environment to internal ...

What is the best way to generate a switch statement based on an enum type that will automatically include a case for each enum member?

While Visual Studio Professional has this feature, I am unsure how to achieve it in VS Code. Take for instance the following Colors enum: enum Colors { Red, Blue, When writing a switch statement like this: function getColor(colors: Colors) { swi ...

How can I change the CSS class of my navbar component in Angular 2 from a different component?

Here is a custom progress bar component I created: @Component ({ selector: 'progress-bar', templateUrl: './progress-bar.component.html', styleUrls: ['./progress-bar.component.css'] }) export class ProgressBarComponent ...

Show information retrieved from one API request within another API request

Currently, I am in the process of retrieving data from the Youtube API by utilizing 2 separate requests. One request is used to fetch a list of videos, while the other request provides details for each individual video. The initial request successfully di ...

Middleware for Redux in Typescript

Converting a JavaScript-written React app to Typescript has been quite the challenge for me. The error messages are complex and difficult to decipher, especially when trying to create a simple middleware. I've spent about 5 hours trying to solve an er ...

Object.assign versus the assignment operator (i.e. =) when working with React components

Just a quick question: I've come across some answers like this one discussing the variances between Object.assign and the assignment operator (i.e. =) and grasp all the points made such as object copying versus address assignment. I'm trying to ...

Navigating to the next page on a dynamic component in Angular 5 by

I'm uncertain if this scenario is feasible, but I have a page that fetches a list of items from an external API. There are currently 5 elements on the page, each acting as a link to its individual dynamically generated page through query strings. For ...

Disable the default animation

Is there a way to disable the default animation of the Select label in my code snippet below? export default function TicketProfile(props: any) { return ( <Container> <FormControl sx={{ ml: 1, mr: 1, minWidth: 220 }}> <Inp ...