根据泛型参数返回函数签名

Posted

技术标签:

【中文标题】根据泛型参数返回函数签名【英文标题】:Returning a function signature based on generic parameters 【发布时间】:2019-10-09 01:48:26 【问题描述】:

我有一个函数createRequest

function createRequest(method: string, path: string) 
  return function resourceApiCall() 
    // ...additional logic
    return httpCall(path, method) 
  

返回一个我想调用的函数resourceApiCall

const fetchUsers = createRequest('GET', '/users')

await fetchUsers(createdAfter: new Date())

我也想做类似的事情:

const fetchPayment = createRequest('GET', '/payments')

await fetchPayment('id', createdAfter: new Date())

我的问题是,如何将定义传递给createRequest,以便fetchUsersfetchPayment 在IDE 中显示正确的函数参数和返回值(任何类型检查都正确)?强>

我认为我需要执行以下操作:

interface FetchPayment 
  (id: string, createdAfter: Date): Promise<id: string>


const fetchPayment = createRequest<FetchPayment>('GET', '/payments')

但我最理想的做法是:

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) 
  

【问题讨论】:

【参考方案1】:

你可以这样继续:

// 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

在上面我使用接口Requests 在类型级别指定从请求路径到您希望createRequest() 返回的函数签名的映射。而createRequest() 是一个generic 函数,使用Requests 来强类型化返回的函数。请注意,在resourceApiCall() 的实现中,我还使用一些内置的conditional types 将argument types 和return type 从函数签名中提取出来。这不是绝对必要的,但会使resourceApiCall() 中的类型更加明确。

无论如何,希望对您有所帮助。祝你好运!


更新:这是一种将其拆分为不同模块的可能方法,以便每个模块只触及自己的端点。

首先,让您的文件中包含createRequest(),以及最初为空的Requests 接口:

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

然后你可以为你的 User 东西制作一个模块:

Requests/user.ts

export interface User 
  name: string;
  age: number;

declare module './requests' 
  interface Requests 
    "/users": (clause:  createdAfter: Date ) => Promise<Array<User>>;
  

还有你的Payment 东西:

Requests/payment.ts

export interface Payment 
  id: string;

declare module './requests' 
  interface Requests 
    "/payments": (id: string, clause:  createdAfter: Date ) => Promise<Payment>;
  

等等。最后,用户可以通过导入 createRequest 以及可能的 userpayment 模块来调用它们(如果它们中有代码,则需要在模块中运行):

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

好的,希望再次有帮助。

【讨论】:

这是一个很大的帮助@jcalz!有没有一种方法可以让我在哪里声明预期的 args 并返回内联类型,而不是使用 interface Requests 我不确定我是否理解...您可以使用类型别名 (type Requests = "/users": (clause: createdAfter: Date ) =&gt; Promise&lt;Array&lt;User&gt;&gt;; ...) 但您可能希望为它使用 some 名称,因为 @ 987654347@ 在签名和实现中多次出现,一遍又一遍地编写该类型可能很烦人。如果我更了解您的用例,我可能会提出建议。为什么不想要一个Requests 接口或类型? 我想共同定位各个类型定义,因为我正在创建一个内部客户端 sdk(http 调用的抽象)。我正在遵循大多数大型公司使用的模式,尤其是条纹模式。在这个例子github.com/stripe/stripe-node/blob/master/lib/resources/… 中,他们有一个Charges 资源,它只是创建了一个端点。我希望我的看起来完全像 + 内联预期的 args 和端点的返回值。我的createRequest 相当于 Stripe 的stripeMethod。再次感谢您的帮助! 那么我可以建议保留Requests 接口并使用declaration merging 将端点添加到Requests 吗?让我看看我是否可以重构上面的代码来证明... 更新了我的答案,希望对您有所帮助。【参考方案2】:

您可以结合使用别名和重载来使其正常工作。基本上将这些参数别名为字符串文字类型,然后为您的函数提供多个签名。然后 TypeScript 可以根据传入的参数推断 createRequest 的返回类型

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');

总之,这将允许createRequest 函数根据传递的第二个参数返回具有正确签名的函数。 Read more about function signatures here,按 ctrl+f 并搜索“Overloads”以了解有关重载的更多信息。

【讨论】:

以上是关于根据泛型参数返回函数签名的主要内容,如果未能解决你的问题,请参考以下文章

函数签名

委托泛型委托多播委托匿名函数lamda表达式事件

返回类型为协议的泛型函数与参数和返回类型为协议的非泛型函数的区别

从没有参数的函数返回泛型

返回作为参数提供的函数时保留类型签名

Python函数,它返回自己的带有参数的签名