Reputation: 48630
I am trying to port the open-source library unbounded to TypeScript.
It provides a binder
function that takes a method, binds it to the context along with any arguments, and returns the result with .unbounded
set to the original method.
In JavaScript it is:
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
}
All of my attempts do not meet the following expectations:
// 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
I thought that TypeScript 3.2 CallbableFunction would have helped here, but I cannot figure out how to use it. It also seems TypeScript has a bug with its implementation that prevents the return types being accurate.
What am I doing wrong?
Upvotes: 2
Views: 3205
Reputation: 249516
We can take the overloads of bind
introduced in 3.2 and modify them to forward the unbounded
property you add to the function.
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
Upvotes: 3