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;
}