What could be causing the type errors I am encountering while trying to resolve this Promise within a generic function?

I am attempting to implement additional types within this WebSocket protocol:

type Action = {
    action: "change-or-create-state";
    response: string;
} | {
    action: "get-state";
    response: string | null;
};

/**
 * map an action to its response
 */
type ActionResponse<T extends Action["action"]> = Extract<
    Action,
    { action: T }
>["response"];

export async function doAction<
    TAction extends Action["action"]
>(action: TAction): Promise<ActionResponse<TAction>> { /* ... */ }

I am facing difficulties with the types when it comes to actually resolving that promise, though:

// data received over websocket in reality
const someDataFromAPI: string = '"hello world"'

export async function doAction<
    TAction extends Action["action"]
>(action: TAction): Promise<ActionResponse<TAction>> {
    return new Promise<ActionResponse<TAction>>((resolve) => {
        const jsonWithSpecificType: ActionResponse<TAction> = JSON.parse(someDataFromAPI);

        /** ⚠️
        Argument of type 'ActionResponse<TAction>' is not assignable to parameter of type 'ActionResponse<TAction> | PromiseLike<ActionResponse<TAction>>'.
        Type 'Extract<{ action: “get-state”; response: string | null; }, { action: TAction; }>["response"]' is not assignable to type 'ActionResponse<TAction> | PromiseLike<ActionResponse<TAction>>'.
        Type 'string | null' is not assignable to type 'ActionResponse<TAction> | PromiseLike<ActionResponse<TAction>>'.
        Type 'null' is not assignable to type 'ActionResponse<TAction> | PromiseLike<ActionResponse<TAction>>'.(2345)
        */
        resolve(jsonWithSpecificType)
    })
}

Playground link here. What could be causing the issue? Using any resolves the problem, but I would like to understand the underlying cause. My assumption is that there might be a discrepancy in maintaining the same type for TAction within and outside the arrow function.

Answer №1

After conducting thorough research, I have come across a potential answer to this query in microsoft/TypeScript#47233. This reference points out an issue that is similar to the one being discussed here and is categorized as a bug. It remains uncertain whether this is indeed a bug or simply a limitation based on design. If time permits, I plan to raise a new inquiry to obtain an authoritative resolution.

My analysis suggests that the complexity arises due to the involvement of generic types such as ActionResponse<TAction> and

ActionResponse<TAction> | PromiseLike<ActionResponse<TAction>>
. The compiler struggles to fully evaluate these types because they incorporate various type manipulations like union types, conditional types, and indexed accesses outlined in the TypeScript documentation. Consequently, ActionResponse<TAction> and
ActionResponse<TAction> | PromiseLike<ActionResponse<TAction>>
are perceived as somewhat "opaque" by the compiler.

While a human reviewer can easily deduce that the former type must be assignable to the latter, following the rule "X is always assignable to X | Y" with ActionResponse<T> mapped to X and

PromiseLike<ActionResponse<TAction>>
represented as Y, the compiler lacks this intuition. When the compiler reaches a point where it struggles with evaluating the types before applying the rules of union assignability, errors tend to occur.

To illustrate this behavior, consider a simplified scenario:

type F<K extends "a" | "b"> = (
    ({ x: "a"; y: "a"; } | { x: "b"; y: "b"; }) & { x: K }
)["y"];

function foo<K extends "a" | "b">(fk: F<K>): F<K> | undefined {
    return fk; // error?!
}

In this example, although it seems evident that F<K> can be assigned to

F<K> | undefined</code regardless of its specific value, the intricate definitions within <code>F
create complexity for the compiler, leading to potential errors. While I utilize an intersection approach instead of Extract for demonstration purposes, the underlying concept remains consistent.

This explanation reflects my current understanding of the situation based on extensive analysis. Should further insights emerge from a formal investigation, I will revise this response accordingly.

Explore Code in TypeScript Playground

Answer №2

It's a temporary fix for now, but I'm not completely certain why it works -

export async function performAction<
    Action extends Action["action"],
    Response = ActionResponse<Action> // <--
>(action: Action): Promise<Response> {
    return new Promise((resolve) => {
        const specificTypeData: Response = JSON.parse(someDataFromAPI)  
        resolve(specificTypeData)
    })
}
const x = performAction("change-or-create-state") // : Promise<string>
const y = performAction("get-state") // : Promise<string | null>
const z = performAction("foo") // : "foo" is not assignable to parameter of type "change-or-create-state" | "get-state"

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

Issue with TranslateModule Configuration malfunctioning

I have put together a file specifically for managing ngx-translate configuration: import { Http } from '@angular/http'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateLoader, TranslateModu ...

I'm encountering difficulty accessing the Index value within the template's Ref

I'm having trouble accessing the index value inside the templateRef. It shows as undefined in the console. <ng-container *ngFor="let notification of notifications; let i = index"> <ng-template *ngTemplateOutlet="notificationPage ...

Utilizing Eithers to effectively manage errors as they propagate through the call chain

I'm delving into functional programming and exploring different ways to handle errors beyond the traditional try/catch method. One concept that has caught my attention is the Either monad in various programming languages. I've been experimenting ...

Uniform retrieval function for interaction with API

I have developed my server backend using PHP and now I am looking to establish communication between the frontend (typescript) and backend. For each of my API requests, I desire to receive a standardized response. Hence, every response from the server fol ...

Error in cypress configuration occurs when a plug is mistakenly defined as an import instead of a const within the cypress.config.ts file

Hello, I am new to using Cypress so please bear with me if this seems like a trivial issue. Below you will find my cypress.config.ts file where I am attempting to integrate the cypress esbuild preprocessor into the configuration. import cypress_esbuild_pre ...

Navigating a SwipeableDrawer in React with scrolling functionality

I'm currently using a swipeable drawer in React from MUI to display lengthy content. My goal is to keep the title visible even when the drawer is closed, and I was able to achieve this through the following method: MUI SwipeableDrawer I've provi ...

Prisma DB is a versatile database that excels in handling m-n

In my database, I have implemented a structure with 3 tables: Member, Characters, and MemberCharacters. Each member can have multiple Characters, and each Character can be used by multiple Members. To handle this many-to-many relationship, I have utilized ...

What is the reason for the removal of HTML tags in the environment when converting Angular dependencies from es2015 to esm2015 during

After completing the generation of the browser application bundle in Intellij, I noticed that the HTML tags cannot be resolved anymore. What could be causing this issue? I also discovered that if I don't include the AngularMaterialModule in the AppMo ...

A guide to sorting through in-app notifications in REACT-NATIVE based on their read status

Incorporating two headings, "Unread" and "Read", into the notification system is my goal. When opened, the Unread Notifications should be displayed beneath the Read notifications. This data is being retrieved from an API. Each notification contains a key ...

Document: include checksum in HTML

I have a set of three files. The file named loader.js is responsible for creating an iframe that loads another file called content.html, which in turn loads content.js. I have made loader.js publicly available so that other users can include it on their ow ...

Building a hybrid application in Angular using UpgradeModule to manage controllers

I am currently in the process of upgrading a large AngularJS application using UpgradeModule to enable running AngularJS and Angular 6 simultaneously without going through the preparation phase, which typically involves following the AngularJS style guide. ...

Tips on invoking Bootstrap's collapse function without using JQuery

We are facing a challenge with our TypeScript files as we have no access to jQuery from them. Our goal is to trigger Bootstrap's collapse method... $(object).collapse(method) but without relying on jQuery. Intended Outcome //Replicates the functio ...

What is the best way to incorporate this in a callback function?

Utilizing a third-party component requires creating an object for configuration, such as itemMovementOptions in the given code sample. export class AppComponent implements OnInit { readonly itemMovementOptions = { threshold: { horizontal: ...

Angular 2 Validation Customizer

Recently, I created a web API function that takes a username input from a text field and checks if it is already taken. The server response returns Y if the username is available and N if it's not. For validating the username, I implemented a Validat ...

What sets apart extending and intersecting interfaces in TypeScript?

If we have the following type defined: interface Shape { color: string; } Now, let's explore two different methods to add extra properties to this type: Using Extension interface Square extends Shape { sideLength: number; } Using Intersection ...

Exploring the Potential of Jest Testing for Angular 6 Services

Hey there, I seem to be facing a bit of a roadblock and could use some assistance. Here's the situation - I'm trying to test a service using Jest, but all the tests pass without any issues even when they shouldn't. Here are the details of t ...

Disable inline imports when implementing an interface in vscode by selecting the "Implement interface" option

When using TypeScript, if I perform an auto-fix on a class name by selecting "Implement interface", it will generate the methods with inline imports like this: getInbox(): Observable<import('../../model/Message').Interactions[]> { t ...

Guide to incorporating external code in InversifyJS without direct control

I'm wondering if it's feasible to add classes that are not editable. Inversify seems to rely heavily on annotations and decorators, but I'm curious if there is an alternative method. ...

Steps to modify the CSS of a custom component in Angular 8

I have been attempting to override the css of a custom component selector, however, my attempts have been unsuccessful. I have tried using ":ng-deep" but it hasn't worked. How can I go about finding a solution for this issue? app.component.html: < ...

Next.JS-13 has encountered an inappropriate middleware in its App Based Routing functionality

I am currently working on developing a user authentication system that includes features for Login, Signup, and Logout. As part of this project, I have created a middleware.ts file in the src directory, which is located next to the app directory. Below is ...