Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - Generic type extending itself

I recently came across something that looked like this:

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

function foo <T extends Test<T>>(el: T): T {
  ...
}

I have to say I am a bit perplexed as to what exactly this is, and why something like this would be required. I've been through the Generics section of the Typescript handbook but couldn't find anything similar.

What does that interface achieve that can't be done with something like the following?

interface Test<T>

Can anyone shed some light on this?

like image 450
bugs Avatar asked May 10 '18 15:05

bugs


People also ask

How do you pass a generic class as parameter in TypeScript?

Assigning Generic ParametersBy passing in the type with the <number> code, you are explicitly letting TypeScript know that you want the generic type parameter T of the identity function to be of type number . This will enforce the number type as the argument and the return value.

How do generics work in TypeScript?

Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.

What is the correct way to define a generic class in TypeScript?

TypeScript supports generic classes. The generic type parameter is specified in angle brackets after the name of the class. A generic class can have generic fields (member variables) or methods. In the above example, we created a generic class named KeyValuePair with a type variable in the angle brackets <T, U> .

Does TypeScript support generics?

Generics Classes TypeScript also supports generic classes. The generic type parameter is specified in angle brackets (<>) following the name of the class. A generic class can have generic fields or methods.


2 Answers

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!

like image 89
jcalz Avatar answered Oct 20 '22 05:10

jcalz


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;
}
like image 2
Dhruv Parekh Avatar answered Oct 20 '22 04:10

Dhruv Parekh