Typescript - Generic type extending itself

Without the actual example, I can only speak in generalities. That sort of syntax is what you need in a language like Java that doesn't have polymorphic this types, which I'll get to shortly.


The idea is that you want a generic type that refers to other objects of the same type as its containing class or interface. Let's look at your Test interface:

interface Test<T extends Test<T>> {
  a: number;
  b: T;
}

This describes a linked-list-like structure where the b property of a Test<T> must also be a Test<T>, since T extends Test<T>. But additionally, it must be (a subtype of) the same type as the parent object. Here's an example of two implementations:

interface ChocolateTest extends Test<ChocolateTest> {
  flavor: "chocolate";
}
const choc = {a: 0, b: {a: 1, flavor: "chocolate"}, flavor: "chocolate"} as ChocolateTest;
choc.b.b = choc;

interface VanillaTest extends Test<VanillaTest> {
  flavor: "vanilla";
}
const vani = {a: 0, b: {a: 1, flavor: "vanilla"}, flavor: "vanilla"} as VanillaTest;
vani.b.b = vani;

Both ChocolateTest and VanillaTest are implementations of Test, but they are not interchangable. The b property of a ChocolateTest is a ChocolateTest, while the b property of a VanillaTest is a VanillaTest. So the following error occurs, which is desirable:

choc.b = vani; // error!

Now you know when you have a ChocolateTest that the entire list is full of other ChocolateTest instances without worrying about some other Test showing up:

choc.b.b.b.b.b.b.b.b.b // <-- still a ChocolateTest

Compare this behavior to the following interface:

interface RelaxedTest {
  a: number;
  b: RelaxedTest;
}

interface RelaxedChocolateTest extends RelaxedTest {
  flavor: "chocolate";
}
const relaxedChoc: RelaxedChocolateTest = choc;

interface RelaxedVanillaTest extends RelaxedTest {
  flavor: "vanilla";
}
const relaxedVani: RelaxedVanillaTest = vani;

You can see that RelaxedTest doesn't constrain the b property to be the same type as the parent, just to some implementation of RelaxedTest. So far, it looks the same, but the following behavior is different:

relaxedChoc.b = relaxedVani; // no error

This is allowed because relaxedChoc.b is of type RelaxedTest, which relaxedVani is compatible with. Whereas choc.b is of type Test<ChocolateTest>, which vani is not compatible with.


That ability of a type to constrain another type to be the same as the original type is useful. It's so useful, in fact, that TypeScript has something called polymorphic this for just this purpose. You can use this as a type to mean "the same type as the containing class/interface", and do away with the generic stuff above:

interface BetterTest {
  a: number;
  b: this; // <-- same as the implementing subtype
}

interface BetterChocolateTest extends BetterTest {
  flavor: "chocolate";
}
const betterChoc: BetterChocolateTest = choc;

interface BetterVanillaTest extends BetterTest {
  flavor: "vanilla";
}
const betterVani: BetterVanillaTest = vani;

betterChoc.b = betterVani; // error!

This acts nearly the same as the original Test<T extends Test<T>> without the possibly mind-bending circularity. So, yeah, I'd recommend using polymorphic this instead, unless you have some compelling reason to do it the other way.

Since you said you came across this code, I wonder if it was some code from before the introduction of polymorphic this, or by someone who didn't know about it, or if there is some compelling reason I don't know about. Not sure.


Hope that makes sense and helps you. Good luck!


public static foo<TType extends number | string, T extends Tree<TType>>(data: T[]): T[] {
    console.log(data[0].key);
    return
}


export interface Tree<T> {
    label?: string;
    data?: any;
    parent?: Tree<T>;
    parentId?: T;
    key?: T;
}