Reputation: 7947
I help maintain a JavaScript library that produces spy functions which allow you to inspect how a passed-in function was called (mainly for use in unit testing).
The library creates a function that has additional properties on it that allow you to inspect the calls.
Is it possible to create a TypeScript definition that will allow the function to be passed in to methods that require a function AND have extra properties?
This is invalid, but something like:
class Spy extends function {
wasCalled: () => boolean;
...
}
Which would allow me to pass a spy into a function with this signature:
function subjectUnderTest(callback:() => void) {
...
}
Upvotes: 28
Views: 52846
Reputation: 28717
I wanted to extend a class with a function, too, and worked out a TypeScript-only solution. I am not really sure whether this is a good idea because clever solutions are not always good solutions. YMMV.
Thanks to Mattias Buelens to provide a partial answer! I am building on it.
// same as in the answer of Mattias
interface Spy {
(foo: string, bar: number): boolean // Just an example
wasCalled(): boolean
}
// and now for the real solution!
class Spy {
_wasCalled: boolean
_baz: boolean // Just an example
private constructor(baz: boolean) {
this._wasCalled = false
this._baz = baz
}
wasCalled(): boolean {
return this._wasCalled
}
toString() { return '[object Spy]' }
static create(baz: boolean) {
const f = <Spy>function(this: Spy, foo: string, bar: number): boolean {
// Do your thing here. Use f instead of this!
console.log('wasCalled', f.wasCalled())
f._wasCalled = true
}
const spy = new Spy(baz)
Object.assign(f, spy)
Object.setPrototypeOf(f, Spy.prototype)
return f
}
}
The idea is to create a function and the instance of Spy
, then assign both the prototype and the properties to the function. Return the instance from a static method. A bonus is the toString()
method.
const spy = Spy.create(true)
console.log('calling spy', spy('foo', 42))
console.log('instanceof', spy instanceof Spy)
works as expected.
I don't think that new Spy()
would work because we need to assign to a function not the other way round. And because we can't replace this
we can't make this
a callable. A hypothetical way I see is to extend a class with really a function constructor somehow like this: class Spy2 extends function() {} {}
, but I didn't find a way to get this working.
Upvotes: 4
Reputation: 20159
Yes, the TypeScript handbook calls this a "hybrid type", because it's a combination of a function type and a regular interface.
interface Spy {
(foo: string, bar: number) : boolean; // Just an example
wasCalled() : boolean;
}
var spy : Spy = createASpySomehow();
var result = spy("foo", 123);
if (spy.wasCalled()) {
// ...
}
Upvotes: 66