首页 > 解决方案 > 如何将 TypeScript 泛型与绑定一起使用?

问题描述

我正在尝试将不受限制的开源库移植到TypeScript

它提供了一个binder函数,该函数接受一个方法,将它与任何参数一起绑定到上下文,并返回结果并.unbounded设置为原始方法。

在 JavaScript 中是:

function binder (method, context, …args) {
    const unbounded = method.unbounded || method
    const bounded = method.bind(context, ...args)
    Object.defineProperty(bounded, 'unbounded', {
        value: unbounded,
        enumerable: false,
        configurable: false,
        writable: false
    })
    return bounded
}

但是,尽管进行了多次尝试,但我终其一生都无法弄清楚如何用 TypeScript 来表达这一点。

我所有的尝试都没有达到以下期望:

// arguments should work correctly

function a(this: { local: string }, arg1: string): string {
    return this.local + arg1
}

// should fail:
binder(a, null) // context wrong type
binder(a, {local: 123}) // local wrong type
binder(a, {local: 123}, 'bar') // local wrong type
binder(a, {local: 'foo'})() // arg1 is missing
binder(a, {local: 'foo'}, 123) // arg1 wrong type
binder(a, {local: 'foo'})(123) // arg1 wrong type
binder(a, {local: 123}, 123) // local and arg1 wrong type
binder(a, {local: 123})(123) // local and arg1 wrong type
binder(a, {local: 'foo'})('bar', 123) // extra arg
binder(a, {local: 'foo'})('bar', 'baz') // extra arg

// should pass:
binder(a, {local: 'foo'})
binder(a, {local: 'foo'}, 'bar')()
binder(a, {local: 'foo'})('bar')


// return value should work correctly

// should pass
binder(a, {local: 'foo'}, 'bar')().substr(0)
binder(a, {local: 'foo'})('bar').substr(0)


// extensions should work correctly

interface E extends Function {
    (this: { local: string }, arg1: string): string
    extension?: string
}

const b = function(this: { local: string }, arg1: string): string {
    return this.local + arg1
} as E
b.extension = 'hello'

const bb = binder(b, { local: 'foo' }, 'bar')
// bb.extension should not be present
// bb.unbounded.extension should be present
// bb.unbounded.unbounded should not be present

const bbb = binder(bb, { local: 'foo' }, 'bar')
// bbb.extension should not be present
// bbb.unbounded.extension should be present
// bbb.unbounded.unbounded should not be present

我认为 TypeScript 3.2 CallbableFunction在这里会有所帮助,但我不知道如何使用它。TypeScript 的实现似乎也有一个错误,它会阻止返回类型的准确性。

我究竟做错了什么?

标签: typescriptgenerics

解决方案


我们可以采用3.2bind中引入的重载并修改它们以转发您添加到函数的属性。unbounded

type UnboundFunction<T extends Function, TUnbound> = T & {
    unbounded?: TUnbound
}

type BoundFunction<T extends Function, TUnbound> = T & {
    unbounded: TUnbound
}


type BoundFunctionHelper<TResult extends Function, TUnbound, TArgument> = BoundFunction<TResult, undefined extends TUnbound ? TArgument : TUnbound >;

export function binder<T, R, TUnbound = undefined>(method: UnboundFunction<(this: T) => R, TUnbound>, thisArg: T): BoundFunctionHelper<() => R, TUnbound, typeof method >;
export function binder<T, A extends any[], R, TUnbound = undefined>(method: UnboundFunction<(this: T, ...args: A) => R, TUnbound>, thisArg: T): BoundFunctionHelper<(...args: A) => R, TUnbound, typeof method >;
export function binder<T, A0, A extends any[], R, TUnbound = undefined>(method: UnboundFunction<(this: T, arg0: A0, ...args: A) => R, TUnbound>, thisArg: T, arg0: A0): BoundFunctionHelper<(...args: A) => R, TUnbound, typeof method >;
export function binder<T, A0, A1, A extends any[], R, TUnbound = undefined>(method: UnboundFunction<(this: T, arg0: A0, arg1: A1, ...args: A) => R, TUnbound>, thisArg: T, arg0: A0, arg1: A1): BoundFunctionHelper<(...args: A) => R, TUnbound, typeof method>
export function binder<T, A0, A1, A2, A extends any[], R, TUnbound = undefined>(method: UnboundFunction<(this: T, arg0: A0, arg1: A1, arg2: A2, ...args: A) => R, TUnbound>, thisArg: T, arg0: A0, arg1: A1, arg2: A2): BoundFunctionHelper<(...args: A) => R, TUnbound, typeof method>;
export function binder<T, A0, A1, A2, A3, A extends any[], R, TUnbound = undefined>(method: UnboundFunction<(this: T, arg0: A0, arg1: A1, arg2: A2, arg3: A3, ...args: A) => R, TUnbound>, thisArg: T, arg0: A0, arg1: A1, arg2: A2, arg3: A3): BoundFunctionHelper<(...args: A) => R, TUnbound, typeof method>;
export function binder<T, AX, R, TUnbound = undefined>(method: UnboundFunction<(this: T, ...args: AX[]) => R, TUnbound>, thisArg: T, ...args: AX[]): BoundFunctionHelper<(...args: AX[]) => R, TUnbound, typeof method>;
export function binder(method: UnboundFunction<Function, Function>, context: any, ...args: any[]): BoundFunction<Function, Function> {
    const unbounded = method.unbounded || method
    const bounded = method.bind(context, ...args)
    Object.defineProperty(bounded, 'unbounded', {
        value: unbounded,
        enumerable: false,
        configurable: false,
        writable: false
    })
    return bounded
}


function a(this: { local: string }, arg: string): string {
    return this.local 
}


binder(a, null) // fails as this.local is missing
binder(a, {local: 'foo'})() // fails as arg1 is missing
binder(a, {local: 'foo'}, 'bar')() // pass
binder(a, {local: 'foo'})('bar') // pass
binder(a, {local: 'foo'}, 123)() // fail due to incorrect type
binder(a, {local: 123}, 'bar')() // fail due to incorrect type
binder(a, {local: 'foo'})('bar') // fail due to incorrect type
binder(a, {local: 'foo'})('bar', 'bob') // fail due to extra arg
binder(a, {local: 'foo'}, 'bar', 'bob') // unfortunately works, also a problem with the 3.2 implementation of bind. 

// Preserve unbounded type test
function a2(this: { local: string }, arg: string, arg2: string): string {
    return this.local 
}

var unbounded = binder(binder(a2, { local: 'foo'}), undefined, "").unbounded;
var o = { local : "", unbounded };
o.unbounded("", "") // unbounded type reserved trhough mutiple binder calls

推荐阅读