2

I am writing some middleware with the following interface

interface Config {
    callApi<P> (api: ApiClient): Promise<P>,
    transformResponse?(payload: P): any
}

From the above, you can see that the config takes a callApi function which will call an remote APi and return the promise-based result, and then and optional transformResponse that will receive the payload, and apply a transformation to it. The key part is that the transformResponse function should infer the type of the payload, based on the return type of the callApi call

Problem is that the above is not valid, because the P type inside transformResponse can not reach the same variable for the callApi function.

Is there any way to accomplish the above, without having to pass in the API response type like this

interface Config<ApiResponse> {
    callApi (api: ApiClient): Promise<ApiResponse>,
    transformResponse?(payload: ApiResponse): any
}

Example usage

const callCreateComment: Config = {
  callApi (api) => api.createComment(),
  transformResponse(payload) => ({ ...payload // infer the return type of the `api.createComment()` call })
}
Tarlen
  • 3,657
  • 8
  • 30
  • 54
  • This feels like you're asking for [existential types](https://github.com/Microsoft/TypeScript/issues/14466), but I'm not sure. Can you show how you are going to use this and how you want to be *prevented* from using it? Obviously you can just return a `Promise` and use `payload: any`, but there's something you're trying to enforce I guess. Do you really mean `Config`'s `callApi()` function to allow the *caller* to specify `P`? Or is it that `callApi()` will return some unknown-to-the-caller `P` that must be accepted by `transformResponse` on the same object? – jcalz Mar 01 '18 at 20:36
  • I removed the Result type to clean up the example. The main point is that I want to be able to infer/auto-complete the return type of API call inside the `transformResponse` function, without having to manually specify its return type – Tarlen Mar 01 '18 at 20:41

1 Answers1

3

Okay, I think I understand what you want, which is to write out the callApi() method and then have TypeScript use the return type of that method to constrain the parameter of the transformResponse() method. I can't figure out how to get the inference to work with an object literal. It looks like the generic ApiResponse parameter hasn't been resolved in time to contextually constrain the type of the argument to transformResponse().

The way I can get the inference to work is to break the object literal into two pieces and have a function take those pieces as arguments and put them together. This lets us take advantage of the left-to-right inference for contextually-typed function parameters. It will probably make more sense just to show you:

I'm going to assume something like these definitions for you, since you didn't specify them:

interface Comment {
    something: string; // who knows
}
interface ApiClient {
    createComment(): Promise<Comment>;
}

And I will give some type aliases to the methods of Config so I can reuse them:

type CallApi<R> = (api: ApiClient) => Promise<R>;
type TransformResponse<R> = (payload: R) => any;

Finally, here's your Config<R>, which is actually the same as your (edited) version with R instead of ApiResponse:

interface Config<R> {
    callApi: CallApi<R>,
    transformResponse?: TransformResponse<R>
}

So here's the function that makes inference work:

const buildConfig = <R>(
    callApi: CallApi<R>, 
    transformResponse?: TransformResponse<R>
): Config<R> => ({ callApi, transformResponse });

The buildConfig() function takes two arguments. The first is a CallApi<R> and the second is a TransformResponse<R>. The left-to-right contextual type inference for function arguments means it will resolve R from the first argument and then use it in the contextual type for the second argument. Like so:

const callCreateComment = buildConfig(
    (api) => api.createComment(),  // R is inferred as Comment
    (payload) => ({ ...payload })  // payload is now constrained
);

You will notice that as you type that in, the contextual type of payload will be inferred as a Comment. And callCreateComment is typed as Config<Comment> without your having to specify Comment anywhere in that function, as desired.

Playground link

Hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • This is absolutely fantastic and exactly what I was looking for! Much appreciated. Only thing missing is that that the payload is (correctly) inferred as being the Promise where I'd like to have it return just the unwrapped value , not the promise. Is this possible? – Tarlen Mar 02 '18 at 10:22
  • Hmm, `payload` is definitely inferred as just `R`. The return value of `callApi()` is `Promise`, but that's the type definition you specified. If you meant for it to return just `R` you can change the type singature. If you're asking how to take a `Promise` and turn it into an `R`, then that's a [separate question](https://stackoverflow.com/q/14220321/2887218) which has been asked many times, and the short answer is "you can't". The longer answer is "you will need to use async techniques instead". – jcalz Mar 02 '18 at 14:38
  • You're right, I got it working now. Thank you so much! – Tarlen Mar 03 '18 at 10:15
  • You can turn Promise into R using Awaited> – Simon Eliasson Feb 09 '23 at 17:02