高阶函数泛型类型的 TypeScript 默认函数

TypeScript default function for higher-order function generic type

我有一个高阶函数,它根据作为参数传递的通用函数正确推断其 returned 函数的类型,用一个简单的例子可能更容易理解:

function someFunction<T>(f: (s: string) => T) {
  return function(s: string): T {
    return f(s)
  }
}

因此,如果我传递 return 是 string 的函数,returned 函数将正确推断类型:

const funcStr = someFunction((s: string) => s)
//    ^ const funcStr: (s: string) => string

如果我传递一个 return 是 number 的函数,它将 return 一个 return 又是一个数字的函数:

const funcNum = someFunction((s: string) => Number(s))
//    ^ const funcStr: (s: string) => number

现在我想给高阶函数加上一个默认值,但是没能成功:

// Type '(s: string) => number' is not assignable to type '(s: string) => T'.
function someFunction<T>(f: (s: string) => T = (s: string) => Number(s)) {
//                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  return function(s: string): T {
    return f(s)
  }
}

如何向高阶函数添加默认函数,并且在使用另一个函数时仍能推断出类型?

Working TS playground.

从技术上讲,编译器的抱怨是对的。您始终可以将 type assertion to suppress the error on the default function parameter, as well as providing a default type parameter 用于 T,以便编译器在无法从参数推断 T 时知道该怎么做:

function someFunction<T = number>(
  f: (s: string) => T = ((s: string) => Number(s)) as any
) {
  return function (s: string): T {
    return f(s)
  }
}

这将完全按照您的意愿运行:

const fNum = someFunction()
console.log(fNum("3.14159").toFixed(2)); // 3.14
const fStr = someFunction((s: string) => s)

但是这种方法的问题是类型参数T是由someFunction()调用者指定的,而不是由实现指定的。因此,没有什么能阻止恶意 and/or 困惑的调用者做这样的事情:

const fOops = someFunction<string>();

调用者已指定 Tstring,即使 T 的默认值是 number,它已被显式 string 类型。所以编译器认为 fOops(s: string) => string 类型,但当然在运行时我们知道它不会是:

try {
  fOops("oopsie").toUpperCase(); // no compiler error
} catch (e) {
  console.log(e); // fOops(...).toUpperCase is not a function
}

这就是编译器抱怨的原因;它是说不能确定 f 的默认值是适合调用者指定的 T 的类型。

如果您不关心这种可能性,那么这种方法很好并且需要对您的代码进行最少的更改。


如果您想防止出现类似 fOops 的情况,您可以将 someFunction() 更改为具有两个调用签名的 overloaded function

// call signatures
function someFunction(): (s: string) => number;
function someFunction<T>(f: (s: string) => T): (s: string) => T;

如果不带参数调用someFunction(),则调用第一个调用签名,return类型必须是(s: string) => number;此调用签名上根本没有泛型类型参数。

否则,如果您使用回调参数调用它,那么您将调用第二个调用签名;有一个类型参数 T 对应于该回调的 return 类型。

然后实现可以和之前类似,但是你放宽了类型以防止投诉(这里我们将使用 any 作为 return 类型):

function someFunction(f: (s: string) => any = (s: string) => Number(s)) {
  return function (s: string) {
    return f(s)
  }
}

现在您打算支持的电话仍然有效:

const fNum = someFunction()
console.log(fNum("3.14159").toFixed(2)); // 3.14

const fStr = someFunction((s: string) => s)

但是不可能用类型参数调用零参数调用签名,所以你不能再错误地调用它了:

const fOops = someFunction<string>() // error!
// Expected 1 arguments, but got 0.

Playground link to code