Search code examples
typescriptdefinitelytyped

Implementing an interface with a call signature and method returning "this"


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:

  1. How to make a class implement a call signature in Typescript?
  2. TypeScript: Implement interface with both call signature and constructor signature
  3. TypeScript: Implement an interface with both call signature and indexing signature

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.


Solution

  • 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
        }
    })