如何将TypeScript泛型与绑定一起使用?

时间:2018-12-04 12:21:21

标签: typescript generics

我正在尝试将开源库unbounded移植到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
}

However, I can not for the life of me figure out how to express this with TypeScript despite numerous attempts.

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

// 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在这里会有所帮助,但我不知道如何使用它。 It also seems TypeScript has a bug with its implementation that prevents the return types being accurate.

我在做什么错了?

1 个答案:

答案 0 :(得分:2)

我们可以采用3.2中引入的bind的重载,并对其进行修改以转发您添加到函数中的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