Search code examples
typescriptthis

TypeScript polymorphic this type in interfaces


is it possible to emulate TypeScript's polymorphic this type behaviour using just interfaces instead of classes? Suppose I have some code like this:

interface Foo {
  foo(): this;
}

interface Bar extends Foo {
  bar(): this;
}

function foo(this: Foo) { return this; }
function bar(this: Bar) { return this; }

function newFoo(): Foo {
  return { foo };
}

function newBar(): Bar {
  return { ...newFoo(), bar }; // Property 'bar' is missing in type 'Foo' but required in type 'Bar'.
}

The newBar() constructor doesn't type check, since the returntype of the foo() coming from destructured newFoo() is inferred as Foo instead of Bar.

I found that the reverse method of wrapping mixins works:

function fooish<T>(object: T): T & Foo {
  return { ...object, foo };
}

function newBar2(): Bar {
  return fooish({ bar }); // Typechecks fine
}

However, there are some situations where I would much prefer the first method, i.e. have a simple constructor which just returns Foo instead of T & Foo and compose by spreading properties, not by application. Essentially what I'm looking for is inheritance with interfaces, such that this remains typed correctly.

Any ideas?


Solution

  • The polymorphic this type is implicitly generic. The this type is a special type of F-bounded polymorphism called "the curiously recurring template pattern in C++. For a type implementer, this is "some generic subtype of the current type I'm writing" whereas for a type user, this is that type. If you want to implement Foo, you need to make sure that the foo() method returns whatever subtype of Foo is being used later.

    Your code is essentially equivalent to

    interface Foo<T extends Foo<T>> {
      foo(): T;
    }
    interface PlainFoo extends Foo<PlainFoo> {}
    
    interface Bar<T extends Bar<T>> extends Foo<T> {
      bar(): T;
    }
    interface PlainBar extends Bar<PlainBar> {}
    
    function foo(this: PlainFoo) { return this; }
    function bar(this: PlainBar) { return this; }
    
    function newFoo(): PlainFoo {
      return { foo };
    }
    
    function newBar(): PlainBar {
      return { ...newFoo(), bar }; 
      // Property 'bar' is missing in type 'Foo<PlainFoo>' 
      // but required in type 'Bar<PlainBar>'
    }
    

    where now it's hopefully clear what the problem is. Your newBar() purports to return a PlainBar, but that is a Bar<PlainBar>, which is a Foo<PlainBar>, and therefore the foo() method needs to return a PlainBar. But instead it returns a PlainFoo. The this type needs to get narrower in subclasses, that's the whole point.


    So, in order for your code to work with

    interface Foo { foo(): this; }
    interface Bar extends Foo { bar(): this; }
    

    You need foo and bar to return this, not Foo and Bar. That means the generic of this needs to be represented as a generic type parameter explicitly:

    function foo<T extends Foo>(this: T) { return this; }
    function bar<T extends Bar>(this: T) { return this; }
    

    And now things work (and you don't want to annotate newFoo()'s return type as Foo either, since that clobbers the generic:

    function newFoo() {
      return { foo };
    }
    
    function newBar(): Bar {
      return { ...newFoo(), bar };
    }
    

    All this works now, because the polymorphic nature of this is maintained.

    Playground link to code