TypeScript polymorphic this type in interfaces

ghz 8months ago ⋅ 99 views

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?

Answers

Yes, TypeScript allows you to achieve polymorphic behavior similar to the this type in classes using interfaces. However, achieving the exact behavior you want, where a function returning an interface can correctly infer the extended interface, is a bit trickier.

One approach you can take is to define factory functions that return objects conforming to the desired interfaces. Here's how you can do it:

interface Foo {
  foo(): this;
}

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

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

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

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

function createBar(): Bar {
  const base = createFoo();
  return { ...base, bar };
}

In this approach, createFoo and createBar are factory functions that return objects conforming to the Foo and Bar interfaces, respectively. By using a base object and spreading its properties, we can ensure that the returned object has both the properties from the base interface (Foo) and the extended interface (Bar).

This way, when you call createBar(), TypeScript correctly infers the return type as Bar, and you get the desired polymorphic behavior.

This approach allows you to compose objects with the desired interface hierarchy without relying on mixins or other more complex patterns.