Determining function return property type in Typescript by mapping interface argument property

In order to manage messaging between the browser and a web worker, I have developed a generic class. Each side creates a class that can send specific messages and acknowledge them on the other side with a returned result in a payload. The implementation is currently working perfectly, and I've implemented an interface mapping to facilitate message sending.

However, I am facing a challenge when it comes to inferring the return type based on the interface mapping passed as an argument to the function.

Below is a link to the playground showcasing the interfaces, classes, and their implementations.

I am specifically struggling with the 'send' function:

messages.send({ action: MessageAction.Init, data: { scripts: [] } }).then(data => {
  // need to infer the return type from the action.
  console.log(data);
});

Essentially, instead of returning void from the playground, I would like it to automatically determine that 'data' is actually

AcknowledgeMessageData<InitReturn>
because the 'action' property of the argument is 'init' (currently not generic). Therefore, the object structure should look like this:

{
   isSuccess: true,
   result: {
      someRandomProperty: '',
      anotherProperty: 0
   }
}

Different actions would lead to different generic properties for 'result'.

Playground

Currently, I'm unable to figure out how to trigger the inference based on the argument's property and apply it to the return value's property.

Edit: added minimal reproduction

enum Action {
    Ack = 'ack',
    Init = 'init'
}

interface BaseMessage {
    action: Action;
}

interface AckBaseMessage extends BaseMessage {
    action: Action.Ack;
}

interface InitBaseMessage extends BaseMessage {
    action: Action.Init;
}

interface MessageDataMapping {
    [Action.Ack]: AckBaseMessage;
    [Action.Init]: InitBaseMessage;
}

export type Message = { [K in Action]: MessageDataMapping[K] }[Action];

async function send(payload: Message) {
    return thisWouldBeReturnedFromWebWorker(payload);
}

async function thisWouldBeReturnedFromWebWorker(payload: Message): Promise<any> {
    if (payload.action === Action.Init) {
        return {
            action: Action.Init,
            result: {
                somePropertiesHere: true
            }
        };
    }

    return { action: Action.Ack, result: { isSuccess: true } }
}

send({ action: Action.Init }).then(response => {
    response.result.somePropertiesHere

    response.result.isSuccess
});

Answer №1

To tackle this, my approach would involve defining the call signature for send() as a generic function that works on the type parameter K, constrained to Action, which corresponds to the action property of the payload parameter:

declare function send<K extends Action>(payload: Message<K>): Promise<Return<K>>;

Here, the input type is Message<K> and the output type is Promise<Return<K>>. Definitions for Message<K> and Return<K> need to be established. Let's begin with Message<K>:

type Message<K extends Action> =
    { [P in K]: { action: P } }[K]

This type is known as a distributive object type introduced in microsoft/TypeScript#47109. It functions as a mapped type which is immediately indexed into. The goal here is to have Message<Action.Init> represent {action: Action.Init}, and Message<Action.Ack> denote {action: Action.Ack}, while

Message<Action.Init | Action.Ack>
represents the union of Message<Action.Init> and Message<Action.Ack>. This operation distributes over unions within the input.

The definition for Return<K> is slightly more complex as it involves retrieving the result property from a helper interface:

interface ReturnMap {
    [Action.Init]: { somePropertiesHere: boolean };
    [Action.Ack]: { isSuccess: boolean }
}

type Return<K extends Action> = { [P in K]:
    { action: K, result: ReturnMap[K] }
}[K];

Despite the complexity, the concept remains similar. Note that while the current structure of Message<K> may seem intricate for its purpose, additional properties can always be added based on K using something akin to a MessageMap interface, similar to how

ReturnMap</code is utilized.</p>
<hr />
<p>With the defined call signature, calls to <code>send()
now align with your requirements:

send({ action: Action.Init }).then(response => {
    response.result
    //       ^?(property) result: { somePropertiesHere: boolean; }
});
send({ action: Action.Ack }).then(response => {
    response.result
    //       ^?(property) result: { isSuccess: boolean; }
});

However, an issue arises inside the implementation of send() where the compiler fails to recognize the specific return values as the correct generic type:

async function send<K extends Action>(payload: Message<K>): Promise<Return<K>> {
    if (payload.action === Action.Init) {
        return { // error! Type 'Action.Init' is not assignable to type 'K'.
            action: Action.Init,
            result: {
                somePropertiesHere: true
            }
        };
    }

    return { action: Action.Ack, result: { isSuccess: true } } // error!
    //     Type 'Action.Ack' is not assignable to type 'K'.
}

Current limitations in TypeScript prevent the compiler from recognizing changes in the actual value of a generic type K, leading to uncertainties regarding the exact type of the returned value despite narrowing down the type of payload.action. Though there are suggestions for improvement like microsoft/TypeScript#33014, they haven't been incorporated yet.


A workaround involves utilizing type assertions to overcome this hurdle:

async function send<K extends Action>(payload: Message<K>): Promise<Return<K>> {
    if (payload.action === Action.Init) {
        return {
            action: Action.Init,
            result: {
                somePropertiesHere: true
            }
        } as Return<K>;
    }
    return { action: Action.Ack, result: { isSuccess: true } } as Return<K>
}

While this solution suppresses compilation errors by essentially stating "trust me, this return value is of type Return<K>", caution should be exercised to ensure the validity of the assertion.


An alternative workaround involves completing the refactoring process by incorporating both the types and implementations. This strategic update allows for the relationship between input and output types to be implemented effectively:

const ioFuncs: { [P in Action]: (i: Message<P>) => Return<P> } = {
    init: (payload) => ({ action: Action.Init, result: { somePropertiesHere: true } }),
    ack: (payload) => ({ action: Action.Ack, result: { isSuccess: true } });
}

async function send<K extends Action>(payload: Message<K>): Promise<Return<K>> {
    return ioFuncs[payload.action](payload);
}

By explicitly mapping over P in Action, the ioFuncs variable operates based on Message<P> and Return<P>. Ultimately, the implementation of send() entails fetching the appropriate property from ioFuncs and calling it with the payload. Type-checking verifies that ioFuncs[payload.action] adheres to the single type

(i: Message<P>) => Return<P>
. Consequently, as payload is of type Message<P>, the function returns Return<P>.

Although more intricate than type assertions, this method helps detect mistakes that assertions might overlook. In case of errors such as interchanging values within init and ack properties, the compiler will flag and signal the inconsistency:

const badIoFuncs: { [P in Action]: (i: Message<P>) => Return<P> } = {
    ack: (payload) => ({ action: Action.Init, result: { somePropertiesHere: true } }), 
    init: (payload) => ({ action: Action.Ack, result: { isSuccess: true } });
} // errors!

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

Learn how to mock asynchronous calls in JavaScript unit testing using Jest

I recently transitioned from Java to TypeScript and am trying to find the equivalent of java junit(Mockito) in TypeScript. In junit, we can define the behavior of dependencies and return responses based on test case demands. Is there a similar way to do t ...

Verify if the property in every element of the array is not empty

How can you determine if all employees have a non-null value for the SSN property in the given object? Employees: { id: 0, name: "John", SSN: "1234" } { id: 1, name: "Mark", SSN: "1876" } { id: 2, name: "Sue&q ...

Having trouble with Vue i18n and TypeScript: "The '$t' property is not recognized on the 'VueConstructor' type." Any suggestions on how to resolve this issue?

Within my project, some common functions are stored in separate .ts files. Is there a way to incorporate i18n in these cases? // for i18n import Vue from 'vue' declare module 'vue/types/vue' { interface VueConstructor { $t: an ...

Simulating Express Requests using ts-mockito in Typescript

Is there a way to simulate the Request class from Express using ts-mockito in typescript? I attempted the following import { Request, Response } from "express"; const request = mock(Request); const req: Request = instance(request); but encou ...

Following the migration to Typescript, the React component is having trouble locating the redux store props and actions

Here is the structure of my app: export default class App extends Component { render() { return ( <Provider store={store}> <Router> <Header/> ...

What advantages does utilizing Jasmine Spy Object provide in Angular Unit Testing?

I have a question regarding unit testing in Angular using Jasmin/Karma. Currently, I am working with three services: EmployeeService, SalaryService, and TaxationService. The EmployeeService depends on the SalaryService, which is injected into its constru ...

Error encountered: No matching overload found for MUI styled TypeScript

I am encountering an issue: No overload matches this call. Looking for a solution to fix this problem. I am attempting to design a customized button. While I have successfully created the button, I am facing the aforementioned error. Below is my code ...

retrieve data from URL parameters (navigation backward)

When navigating from the main page to the transaction page and then to the details page, I have implemented a go back feature on the details page. Using the state, I pass data when navigating back so that I can access it again from the transaction page. H ...

Troubleshooting issue with Material UI icons in React application with Typescript

I created a set of icons based on a github help page like this: const tableIcons = { Add: forwardRef((props, ref) => <AddBox {...props} ref={ref} />), DetailPanel: forwardRef((props, ref) => ( <ChevronRight {...props} ref={ref} /> ...

The child element is triggering an output event that is in turn activating a method within the parent

I am currently utilizing @Output in the child component to invoke a specific method in the parent component. However, I am encountering an issue where clicking on (click)="viewPromotionDetails('Learn more')" in the child component is al ...

Create a debounce click directive for buttons in a TypeScript file

I'm facing an issue with implementing debounce click on a dynamically added button using TypeScript. I need help with the correct syntax to make it work. private _initActionsFooter(): void { this.actionsFooterService.add([ { ...

Attempting to authenticate with Next.js using JWT in a middleware is proving to be unsuccessful

Currently, I am authenticating the user in each API call. To streamline this process and authenticate the user only once, I decided to implement a middleware. I created a file named _middleware.ts within the /pages/api directory and followed the same appr ...

The module 'AppModule' is throwing an error with the import of 'MatDialogRef' which is causing unexpected value. To resolve this issue, make sure to include a @

I am currently facing an issue while trying to incorporate Angular Material into my Angular project. Despite successful compilation of the program, I encounter an error when running it in the browser. Uncaught Error: Unexpected value 'MatDialogRef&ap ...

Is there a way to determine if a browser's Storage object is localStorage or sessionStorage in order to effectively handle static and dynamic secret keys within a client?

I have developed a customizable storage service where an example is getExpirableStorage(getSecureStorage(getLocalStorage() | getSessionStorage())) in typescript/javascript. When implementing getSecureStorage, I used a static cipher key to encrypt every ke ...

Is it necessary to 'type assert' the retrieved data in Axios if I have already specified the return type in the function declaration?

Consider the code snippet below: import axios from 'axios' async function fetchAPI<T>(path: string, data: any): Promise<T> { return (await axios.get(path, data)).data as T } async function getSomething(): Promise<SomeType> { ...

Tips for Using Typescript Instance Fields to Prevent Undefined Values

After creating a few Typescript classes, I encountered an issue where I would get an undefined error when trying to use them after instantiating. I experimented with initializing my fields in the constructor, which resolved the problem, but I don't t ...

Utilize Angular's Reactive Form feature to track changes in Form Control instances within a Form Array and calculate the total value dynamically

I am currently utilizing Angular Reactive Forms to loop through an array of values and I want to include a total field after the Form Array that automatically updates whenever there are changes in the Form Array control values. Here is some sample data: ...

Sequelize Date Range Error When Using Op.between with TypeScript

My goal is to retrieve all records from a MySql table that were created within a specific date range. To accomplish this, I created the following code snippet: import { Sequelize, Model, DataTypes, Op } from 'sequelize'; const sequelize = new ...

Vite HMR causes Vue component to exceed the maximum number of recursive updates

After making changes to a nested component in Vue and saving it, I noticed that the Vite HMR kept reloading the component, resulting in a warning from Vue: Maximum recursive updates exceeded... Check out the online demo on stackblitz. Make a change in Chi ...

Error encountered in azure devops preventing successful execution: "npm ERR! code ELIFECYCLE"

I am facing an issue with my Azure DevOps build pipeline that contains 2 npm tasks: - one for npm install - and the other for npm run-script build Unfortunately, I am encountering errors that do not provide sufficient information about the root cause of ...