Using TypeScript generic as its own argument, or specifying it as its own default argument

Viewed 372

I have the following ItemBase interface and multiple other recursive xItem interfaces that extend from it:

interface ItemBase<Children extends ItemBase<Children>> {
  x: string;
  y: number;
  children: Children[];
}

interface FooItem extends ItemBase<FooItem> {
  foo: boolean;
}

interface BarItem extends ItemBase<BarItem> {
  bar: "b" | "a" | "r";
}

I would now like to reference ItemBase with itself as its own argument:

const transformItem = (item: ItemBase<ItemBase>) => { ... }
//                                    ^
// TS2314: Generic type 'ItemBase ' requires 1 type argument(s).

but this doesn't work, because since I didn't specify the inner ItemBase's argument (and obviously can't keep specifying those to infinity).

Even better would be providing itself as ItemBase's default argument... I tried two ways, but each of them runs into its own error:

interface ItemBase<Children extends ItemBase<Children> = ItemBase<Children>> {
//                                                                ^
// TS2744: Type parameter defaults can only reference previously declared type parameters.
  x: string;
  y: number;
  children: Children[];
}
interface ItemBase<Children extends ItemBase<Children> = ItemBase> {
//                                                       ^
// TS2716: Type parameter 'Children' has a circular default.
  x: string;
  y: number;
  children: Children[];
}

An obvious workaround would be creating a separate interface to express my self-referencing base:

interface BaseItem extends ItemBase<BaseItem> {}
const transformItem = (item: BaseItem) => { ... }

but that's more of a hack, since the other items don't really inherit from it.

What are some other options I have?

1 Answers

Depending on your use case, you might want to use polymorphic this types. In an interface or class declaration, you can use the type named this to refer to the "current" subtype. It is as if this is an "implicit" generic type parameter which behaves similarly to the recursive constraint (a.k.a. "F-bounded polymorphism") in interface Foo<T extends Foo<T>>. It frees you from having to explicitly use generics:

interface ItemBase {
    x: string;
    y: number;
    children: this[];
}

interface FooItem extends ItemBase {
    foo: boolean;
}

interface BarItem extends ItemBase {
    bar: "b" | "a" | "r";
}

And to see it in action, here are some possible uses:

const fooItem: FooItem = {
    x: "a",
    y: 1,
    foo: true,
    children: [{
        x: "b", y: 2, foo: true, children: []
    }]
}

function processItem(item: ItemBase) {
    console.log(item.x.toUpperCase());
    console.log(item.y.toFixed(2));
    item.children.forEach(i => processItem(i));
}

processItem(fooItem); // A, 1,00, B, 2.00

There are some situations in which polymorphic this is hard to use, but without more concrete use cases in the question I'm not sure if you will run into them.

Playground link to code

Related