Reputation: 1065
I have a function createRequest
:
function createRequest(method: string, path: string) {
return function resourceApiCall() {
// ...additional logic
return httpCall(path, method)
}
}
that returns a function resourceApiCall
that I would like to call like:
const fetchUsers = createRequest('GET', '/users')
await fetchUsers({createdAfter: new Date()})
I would also like to do something like:
const fetchPayment = createRequest('GET', '/payments')
await fetchPayment('id', {createdAfter: new Date()})
My question is, how can I pass a definition to createRequest
so that fetchUsers
and fetchPayment
display the correct function parameters and return value inside the IDE (any type-check correctly)?
I believe I need to do something like:
interface FetchPayment {
(id: string, {createdAfter: Date}): Promise<{id: string}>
}
const fetchPayment = createRequest<FetchPayment>('GET', '/payments')
but I would ideally like to do something like:
const fetchPayment = createRequest<Args, Result>('GET', '/payments')
function createRequest<Args, Result>(method: string, path: string) {
return function resourceApiCall(...args: Args) {
// ...additional logic
return httpCall<Result>(path, method)
}
}
Upvotes: 1
Views: 802
Reputation: 328342
You might proceed like this:
// some interfaces you expect httpCall to return
interface User {
name: string;
age: number;
}
interface Payment {
id: string;
}
// a mapping of request paths to the function signatures
// you expect to return from createRequest
interface Requests {
"/users": (clause: { createdAfter: Date }) => Promise<Array<User>>;
"/payments": (id: string, clause: { createdAfter: Date }) => Promise<Payment>;
}
// a dummy httpCall function
declare function httpCall<R>(path: string, method: string, payload: any): R;
// for now only GET is supported, and the path must be one of keyof Requests
function createRequest<P extends keyof Requests>(method: "GET", path: P) {
return (function resourceApiCall(
...args: Parameters<Requests[P]> // Parameters<F> is the arg tuple of function type F
): ReturnType<Requests[P]> { // ReturnType<F> is the return type of function type F
return httpCall<ReturnType<Requests[P]>>(path, method, args);
} as any) as Requests[P]; // assertion to clean up createRequest signature
}
async function foo() {
const fetchUsers = createRequest("GET", "/users");
const users = await fetchUsers({ createdAfter: new Date() }); // User[]
const fetchPayment = createRequest("GET", "/payments");
const payment = await fetchPayment("id", { createdAfter: new Date() }); // Payment
}
In the above I'm using an interface Requests
to specify at the type level the mapping from the request path to the function signature you want createRequest()
to return. And createRequest()
is a generic function using using Requests
to strongly type the returned function. Notice that inside the implementation of resourceApiCall()
I also use some built-in conditional types to pull the argument types and return type out of a function signature. This is not strictly necessary, but makes the typings inside of resourceApiCall()
more explicit.
Anyway, hope that helps. Good luck!
UPDATE: Here's a possible way to split that up into different modules so that each module only touches its own endpoint.
First, have your file with createRequest()
in it, along with an initially empty Requests
interface:
Requests/requests.ts
export interface Requests extends Record<keyof Requests, (...args: any[]) => any> {
// empty here, but merge into this
}
// a dummy httpCall function
declare function httpCall<R>(path: string, method: string, payload: any): R;
// for now only GET is supported, and the path must be one of keyof Requests
export function createRequest<P extends keyof Requests>(method: "GET", path: P) {
return (function resourceApiCall(
...args: Parameters<Requests[P]> // Parameters<F> is the arg tuple of function type F
): ReturnType<Requests[P]> {
// ReturnType<F> is the return type of function type F
return httpCall<ReturnType<Requests[P]>>(path, method, args);
} as any) as Requests[P]; // assertion to clean up createRequest signature
}
Then you can make a module for your User
stuff:
Requests/user.ts
export interface User {
name: string;
age: number;
}
declare module './requests' {
interface Requests {
"/users": (clause: { createdAfter: Date }) => Promise<Array<User>>;
}
}
and your Payment
stuff:
Requests/payment.ts
export interface Payment {
id: string;
}
declare module './requests' {
interface Requests {
"/payments": (id: string, clause: { createdAfter: Date }) => Promise<Payment>;
}
}
et cetera. Finally a user could call these by importing createRequest
and possibly the user
and payment
modules (if they have code in them you need to run in your module):
test.ts
import { createRequest } from './Requests/requests';
import './Requests/user'; // maybe not necessary
import './Requests/payment'; // maybe not necessary
async function foo() {
const fetchUsers = createRequest("GET", "/users");
const users = await fetchUsers({ createdAfter: new Date() }); // User[]
const fetchPayment = createRequest("GET", "/payments");
const payment = await fetchPayment("id", { createdAfter: new Date() }); // Payment
}
Okay, hope that helps again.
Upvotes: 1
Reputation: 5770
You could combine aliases and overloads to get this working. Basically alias those arguments as string literal types, and then give your function multiple signatures. TypeScript can then infer the return type of createRequest
based on the arguments passed in
type UserPath = '/users';
type PaymentPath = '/payment';
type CreatedAfter = {
createdAfter: Date;
};
function createRequest(
HttpVerb: string,
target: UserPath
): (id: string, date: CreatedAfter) => Promise<{ id: string }>;
function createRequest(
HttpVerb: string,
target: PaymentPath
//I'm just guessing the return type here
): (date: CreatedAfter) => Promise<{ id: string }[]>;
function createRequest(HttpVerb: string, target: UserPath | PaymentPath): any {
//your function implementation doesn't have to be like this, this is just so
//this example is fully working
if (target === '/users') {
return async function(date) {
return { id: '1' };
};
} else if (target === '/payment') {
return async function(id, date) {
return [{ id: '1' }];
};
}
}
//this signature matches your fetchUsers signature
const fetchUsers = createRequest('GET', '/users');
//this signature matches your fetchPayment signature
const fetchPayment = createRequest('GET', '/payment');
In summation, this will allow the createRequest
function to return a function with the correct signature based on the second argument passed. Read more about function signatures here, ctrl+f and search for "Overloads" to learn more about overloading.
Upvotes: 0