首页 > 解决方案 > 带有属性注入的打字稿高阶函数

问题描述

我想知道如何组合将对象作为唯一参数的函数,但每个高阶函数都附加一个属性。例如,我有一个接受上下文的函数。我想用 awithCookies然后是一个函数来包装那个函数withToken,并且可以访问这两个函数cookiestoken最后一个函数。


withCookies(
  withToken(
    (ctx) => {
      console.log(ctx.cookies) // Record<string, string>
      console.log(ctx.token) // string
    }
  )
)

这种高阶函数的打字稿签名是什么?

标签: typescripttypescript-generics

解决方案


您当然可以为withCookies()and编写调用签名withToken(),甚至编写一个withProps()可以生成这两者的函数。但是您会遇到的一个问题是类型推断:编译器可能无法在最里面的函数中根据上下文键入ctx回调参数,因为所讨论的函数需要是泛型的,而泛型类型参数推断通常不会起到很好的作用非常适合上下文输入(请参阅诸如microsoft/TypeScript#33042microsoft/TypeScript#38872之类的问题有关此限制的示例)。为了让您的代码正常工作,您可能需要在此过程中手动指定一些类型,以便编译器了解您在做什么。


首先让我们定义CookiesToken接口为您正在谈论的对象命名:

interface Cookies {
  cookies: Record<string, string>
}
interface Token {
  token: string;
}

那么该withCookies()函数在某些对象类型中应该是通用的T。它接受一个类型的回调函数(ctx: T & Cookies) => void,并返回一个类型的函数(ctx: T) => void。像这样:

/* 
declare const withCookies: <T extends object>(
  cb: (ctx: T & Cookies) => void
) => (ctx: T) => void 
*/

并且具有被替换为withToken()的类似类型:CookiesToken

/* 
declare const withToken: <T extends object>(
  cb: (ctx: T & Token) => void
) => (ctx: T) => void 
*/

签名如此相似意味着您可能有一个withProps()可以将它们吐出的函数。这是编写它的一种方法:

const withProps = <U extends object>(u: U) =>
  <T extends object>(cb: (ctx: T & U) => void) =>
    (ctx: T) => cb({ ...ctx, ...u });

当您使用 type 的参数withCookies调用时,您会得到什么:withProps()Cookies

const withCookies = withProps<Cookies>(
  { cookies: { a: "hello", b: "goodbye" } }
);

并且withTokenwithProps()使用类型参数调用时得到的结果Token

const withToken = withProps<Token>(
  { token: "token" }
);

您可以验证这两个函数是否具有上面声明的调用签名。


所以现在我们尝试调用这些函数并看到编译器对上下文类型不满意:

const badResult = withCookies(
  withToken(
    (ctx) => {
      console.log(ctx.cookies) // error! Property 'cookies' does not exist on type 'never'
      console.log(ctx.token) // error! Property 'token' does not exist on type 'never'
    }
  )
);
// const badResult: (ctx: never) => void

糟糕,编译器推断出内部ctx的类型never是不可能的类型。两者的T参数withCookies()withToken()也被推断为never。泛型和上下文类型推断的交互在这里没有用,因此回调的主体有错误。虽然badResult()在运行时会正常运行,但最好清除编译器错误。


修复错误的一种方法是将ctx回调参数的类型显式注释为Cookies & Token

const result = withCookies(
  withToken(
    (ctx: Cookies & Token) => {
      console.log(ctx.cookies)
      console.log(ctx.token)
    }
  )
);
// const result: (ctx: object) => void

另一种是显式指定一些泛型类型参数;withToken在这种情况下,调用T指定为Cookies有效:

const alsoResult = withCookies(
  withToken<Cookies>(
    (ctx) => {
      console.log(ctx.cookies)
      console.log(ctx.token)
    }
  )
);
// const alsoResult: (ctx: object) => void

无论哪种方式都会产生一个接受任何对象的最终函数。当我们测试它时,我们可以看到传入的对象withProps()已经按照需要传播到我们的内部回调中:

result({});
/*
[LOG]: {
  "a": "hello",
  "b": "goodbye"
} 
[LOG]: "token" 
*/

Playground 代码链接


推荐阅读