Preface: Our team is working on a library built on top of d3. Since we are using TypeScript, we are also using d3's types from DefinitelyTyped. The following question arises when trying to work with interfaces such as ScaleOrdinal and many others from there.
Suppose we have an interface containing both a call signature and additional properties:
export interface Foo<T> {
// Let's pretend this will be the identity function
(arg: T): T;
// Let's pretend that this will be a no-op function
// Note that this returns "this"
doFoo(): this;
}
How can we implement such an interface correctly & in a type-safe manner[1]? Upon research I've found the following related questions, all of which are slightly different and/or fairly old. I'd like to get an idea of whether we're missing something or whether to raise an issue with the TypeScript team here:
Note that the interface is external to us, thus implementing it is our only option.
¹ For the sake of the question, I would like the implementation to explicitly restate all type annotations.
In recent versions of typescript (3.2 or 3.3 not sure which) when you declare a function, you can also assign extra properties to the function and typescript will consider these as the definition for those properties and not complain about them not having been defined:
export interface Foo<T> {
(arg: T): T;
doFoo(): this;
}
function foo(arg: number) : number {
return arg
}
foo.doFoo = function <TThis extends typeof foo>(this: TThis): TThis { // no polymorphic this in simple functions
return this
}
let o: Foo<number> = foo; // foo is compatible with Foo<number>
The old was of doing it, which still works is using Object.assign
to create the function with extra properties:
let o: Foo<number> = Object.assign(function (arg: number): number {
return arg
}, {
doFoo: function <TThis>(this: TThis): TThis {
return this
}
})