typescript:基于函数参数计数的重载

z31licg0  于 2023-02-17  发布在  TypeScript
关注(0)|答案(1)|浏览(111)

我想实现以下内容:
创建一个函数F,它接受函数G作为第一个参数,并且:

  • 如果G有1个或多个参数,F接受参数列表作为第二个参数
  • 如果G有0个参数,则F不接受任何其他参数

我第一次天真地尝试实现它是这样的:

type F0 = () => any
type F1 = (x: any, ...xs: any[]) => any

function f<F extends F0>(f: F): void
function f<F extends F1>(f: F, xs: Parameters<F>): void

这并不十分有效,给出以下结果:

f(() => {}) // ok
f((a: number) => {}, [2]) // ok

f((a: number) => {}, ['a']) 
// Error: Type 'string' is not assignable to type 'number'.
// Good, working as expected

f((a: number) => {})
// Error: Argument of type '(a: number) => void' is not assignable to parameter of type 'F0'
// So it sees this as the first overload. It doesn't ask for a missing second arg.
// Technically, suits my goals, but in a kinda wrong way.

f(() => {}, []) // ok (!)
// Somehow it sees this as valid application of the second overload, 
// regardless of parameters count

问题是,F0 extends F1 ? true : false给出了true的结果-这就是为什么最后一种情况被认为是第二种重载。
我明白这里没有错,这是TS的正确行为。
但到那时我的任务有可能完成吗?

dxxyhpgq

dxxyhpgq1#

编写f()的调用签名的一种方法如下:

declare function f<A extends any[]>(
    cb: (...a: A) => any, ...rest: ArgsOrNothing<A>
): void;

type ArgsOrNothing<A> =
    [xs: A & [any, ...any]] |
    ([] extends A ? [] : never)

它不是显式重载,而是回调函数的rest参数元组类型A中的generic,然后我们计算ArgsOrNothing<A>以确定f()是否应该接受另一个参数。由于ArgsOrNothing<A>是一个联合类型,f()的行为就像一个重载函数。
第一种可能性:[xs: A & [any, ...any]],这意味着f()接受A & [any, ...any]]类型的附加参数。[any, ...any]类型是具有rest元素的元组,这意味着必须知道它至少包含一个元素。因此,A & [any, ...any]类型的值既适合作为cb的参数元组,* 和 * 必须包含至少一个元素,这意味着这种可能性是用于使用两个参数调用f(cb, xs),其中xs非空。
另一种可能性:([] extends A ? [] : never)。这是一个条件类型,用于检查空参数列表[]是否可赋值给A ...换句话说,cb()是否是可接受的调用。如果是,则f()应该可以仅使用一个参数进行调用(通过将[]添加到...rest的类型)。如果不是,则f()应该 * 不 * 是仅用一个参数可调用的(通过不将任何东西添加到...rest的类型;| never不改变任何东西)。这意味着这种可能性是在cb()可以接受的情况下,用一个参数调用f(cb)
让我们来测试一下:

f(() => { }) // ok
f((a: number) => { }, [2]) // ok
f((a: number) => { }, ['a'])  // error
f((a: number) => { }) // error
f(() => { }, []) // error

const cb = (a?: number) => { }
f(cb); // okay
f(cb, [1]); // okay
f(cb, []); // error

const cb2 = (...args: string[]) => { }
f(cb2); // okay
f(cb2, ["a", "b", "c"]); // okay
f(cb2, []); // error

我认为这些看起来都不错,标准的情况可以做你想做的,对于边缘情况,比如cb()有可选参数或者它是可变参数,上面的行为是你应该被允许以两种方式调用它,但是如果你想调用底层函数cb()而不带参数,你就不能给f()传递第二个参数。
Playground代码链接

相关问题