Alter the type of a function signature while retaining its generics

Initially, I was uncertain about what to search for, so it's possible that the question has already been answered elsewhere. However, after 2 days of testing and researching, I still couldn't find a solution...

I am in the process of creating a proxy that will call a backend with a specific type. The challenge arises when synchronous methods need to be converted into asynchronous ones, in order to maintain type safety on the client side by changing function signatures (similar to "promisify") from sync to async. While this transition works smoothly for regular functions, generic types in signatures get lost and become unknown...

Even with the latest TypeScript version available (currently 4.7.3), I'm unsure if there is a way to achieve this. Perhaps a TypeScript expert out there has the magic solution?

The objective is to have

const syncIdentity = <T>(o: T) => o;
// type is <T>(o:T) => T
const asyncIdentity = async <T>(o: T) => o;
// type is <T>(o:T) => Promise<T>

type PromisifiedSyncIdentity = Promisify<typeof syncIdentity>;
// want <T>(o:T) => Promise<T>
type PromisifiedAsyncIdentity = Promisify<typeof asyncIdentity>;
// want <T>(o:T) => Promise<T>

My initial attempt was:

type Promisify<F extends (...args: any[]) => any> = (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;

type PromisifiedSyncIdentity = Promisify<typeof syncIdentity>;
// want <T>(o:T) => Promise<T>
// received (o:unknown) => Promise<unknown> :-/
type PromisifiedAsyncIdentity = Promisify<typeof asyncIdentity>;
// want <T>(o:T) => Promise<T>
// received (o:unknown) => Promise<unknown> :-/

The second approach maintains generics for functions that are already async (retains the original type)

type Promisify<F extends (...args: any[]) => any> = F extends (...args: any[]) => infer R
    ? R extends Promise<any>
        ? F
        : (...args: Parameters<F>) => Promise<ReturnType<F>>
    : never;

type PromisifiedSyncIdentity = Promisify<typeof syncIdentity>;
// want <T>(o:T) => Promise<T>
// received (o:unknown) => Promise<unknown> :-/
type PromisifiedAsyncIdentity = Promisify<typeof asyncIdentity>;
// want <T>(o:T) => Promise<T>
// received <T>(o:T) => Promise<T> (YEAH! :-D)

That concludes my efforts! If any skilled TypeScript developer has a solution to preserve generics while changing function signatures, I would greatly appreciate the insight. Otherwise, confirming that it's not feasible would also be helpful.

Answer №1

Your understanding of the mapped type approach is almost there, you just need to incorporate conditional wrapping of the return value in a promise:

type Promisify<FuncType extends (...args: any[]) => any> =
    // This condition is used for mapping and inferring types
    FuncType extends (...args: infer ArgTypes) => infer RetType
    ? RetType extends Promise<any>
        // If it already returns a promise, keep as is
        ? FuncType
        // If it doesn't return a promise, wrap the return type in a promise
        : (...args: ArgTypes) => Promise<RetType>
    // This will never happen due to our type constraint
    : never;

You can then define your types using type parameters like this:

type PromisifiedSyncIdentity<T> = Promisify<typeof syncIdentity<T>>;
//   ^?
// expect <T>(o:T) => Promise<T>
type PromisifiedAsyncIdentity<T> = Promisify<typeof asyncIdentity<T>>;
//   ^?
// expect <T>(o:T) => Promise<T>

For testing purposes, an example usage could be:

const f: PromisifiedSyncIdentity<string> = async (o: string) => o; // It Works!

If dealing with more type parameters, simply handle that when creating the new type; the core logic of Promisify remains the same:

type SyncWithTwoTypeParams<A, B> = (o: A) => B;

type PromisifiedSyncWithTwoTypeParams<A, B> = Promisify<SyncWithTwoTypeParams<A, B>>;

const f2: PromisifiedSyncWithTwoTypeParams<string, number> = async (o: string) => {
    return Number(o);
}; // Works!

Interactive Example Link

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

Encountering an issue with TypeScript error code TS2322 when trying to assign a className to the @

Encountering a typescript error when trying to apply a className to a Box element. Interestingly, the same code works on other developers' machines with almost identical configurations. Current dependencies: "@material-ui/core": "4.11. ...

Tips for showing that every field in a typed form group must be filled out

Starting from Angular 14, reactive forms are now strictly typed by default (Typed Forms). This new feature is quite convenient. I recently created a basic login form as shown below. form = this.fb.group({ username: ['', [Validators.required ...

Angular is failing to show any data on the display, despite there being no apparent errors

As a newcomer to Java and Angular, I am currently enrolled in a course on getting started with Angular. I have been attempting to display information in the navigator, but for some reason, nothing is showing up. Despite thoroughly checking my code, I could ...

The origin of the Angular img src becomes blurred when invoking a function

I want to dynamically change the image src by calling a function that returns the image path. However, when I attempt to do so using the code below, the image element displays as <img src(unknown)/> component.ts: getMedia(row) { this.sharedData ...

Error encountered when initializing a variable within the constructor of a TypeScript file in Angular 4

This is the content of my app.component.html file PL Auth Username: Password : Generate OTP Enter OTP : Login This is the code in my app.component.ts file import { Component, OnInit } from '@angular/core' ...

A guide to teaching TypeScript to automatically determine the type of dynamic new() calls

One of the challenges I'm facing involves dynamically creating subclasses and ensuring that the factory function is aware of the subclass's return type. While I can currently achieve this using a cast, I am exploring options to infer the return ...

Angular's promise is incompatible with the type ts2322 and cannot be assigned

Struggling to implement a login feature in Angular, encountering an error related to promises: "Type 'Promise<ApiResponse<UserLogged> | undefined>' is not assignable to type 'Promise<ApiResponse<UserLogged>>&apos ...

Error in React: Trying to access property 'functionName' of an undefined object on click event

I am facing an issue while trying to click a button in my React component that is supposed to trigger a function with a parameter named "item" which is defined within the function. The pseudo-HTML snippet for this scenario looks like: <div>{item.cre ...

What might be causing AngularJS to fail to display values when using TypeScript?

I have designed the layout for my introduction page in a file called introduction.html. <div ng-controller="IntroductionCtrl"> <h1>{{hello}}</h1> <h2>{{title}}</h2> </div> The controller responsible for handling th ...

Tab-based Ionic 2 advertising campaign featuring banners

Is there a way to incorporate an advertisement banner image above the tabs in ionic 2? Any suggestions on how I can achieve this or create the banner in that specific position? ...

HttpClient service not available

Upon transitioning from angular 4.4 to 5.0 and updating all HttpModule to HttpClientModule, an error started to occur. Despite re-adding HttpModule to rule out any dependency issues, the error persisted. My app.module is correctly configured as follows: ...

Full width divider for Ionic 2 lists

I am currently facing an issue with my Ionic 2 app where the divider is only full width on the last element in the list. I would like all elements to have a full width border, but I couldn't find any information about this in the documentation. Any he ...

How to dynamically retrieve values from a const object literal using TypeScript

Currently, I am utilizing a TypeScript library known as ts-proto, which is responsible for generating TypeScript code. The resulting generated code resembles the following: //BasicMessage.ts export interface BasicMessage { id: Long; name: string; } ...

Caution: The absence of FIREBASE_CONFIG and GCLOUD_PROJECT environment variables may result in the failure to initialize firebase-admin

I followed a tutorial to set up the Firebase Admin SDK. https://firebase.google.com/docs/admin/setup I downloaded a JSON file (service account) from the Firebase console located at: C:\ct\functions\src\cargo-tender-firebase-adminsdk- ...

Converting the source to your image assets in Angular: A step-by-step guide

I am looking to update the source code. Here is an example: <div *ngFor="let item of meal.allergenList" class="btn btn-primary"> <img [src]="item" alt=""> </div> I want to make the following co ...

Error TS2322: The specified type Login cannot be assigned to the given type

I've been facing an issue while working on my app in react native. The error message I keep encountering is as follows: TS2322: Type 'typeof Login' is not assignable to type ScreenComponentType<ParamListBase, "Login"> | undefined Type ...

What is the best way to decouple the data layer from Next.js API routes?

Currently, I am working on a project using Next.js with MongoDB. My setup involves using the MongoDB client directly in TypeScript. However, I have started thinking about the possibility of switching to a different database in the future and how that would ...

Having trouble sending a x-www-form-urlencoded POST request in Angular?

Despite having a functional POST and GET service with no CORS issues, I am struggling to replicate the call made in Postman (where it works). The only thing I can think of is that I may have incorrectly set the format as x-www-form-urlencoded. When searchi ...

Unexpected error arises in Typescript despite code functioning properly

As part of a practice project where I'm focusing on using webpack, ES6, npm and Typescript implementation, I have successfully set up loaders for 'awesome-typescript-loader' and 'babel-loader', which are bundling the code properly. ...

Next.js typescript tutorial on controlling values with increment and decrement buttons

I'm just starting to learn typescript and I'm looking to implement increment and decrement buttons in a next.js project that's using typescript. export default function Home() { return ( <div className={styles.container}> ...