2

I have the code below as a generic function to get all parents in a Tree type of structure.

When I use it in a class though, I'm getting a Type Error.

type GetParentFunc<T> = (x:T)=>T;


function getAllParents<T>(obj:T, getParent:GetParentFunc<T>)
{
    let result:T[] = [];

    let parent = getParent(obj);

    while ( parent != null )
    {
        result.push( parent );
        parent = getParent(parent);
    }

    return result;
}

class Category
{
    parent:Category;

    // doesn't work
    parentsBroken = () =>
    {
        return getAllParents(this, x => this.parent);
    }

    // works
    parentsFixed = () =>
    {
        return getAllParents<Category>(this, x => this.parent);
    }
}

T is getting filled in with this - and I get the following error about the x => x.parent argument.

Type 'Category' is not assignable to type 'this'.

This is easily fixable like:

getAllParents<Category>(this, x => x.parent)
getAllParents(this as Category, x => x.parent)

But I am a visual noise minimalist, and would rather spend some time so I can get rid of this noise for the foreseeable future.

Is there a way to exclude the this Type from a generic?

Like:

export function getAllParents<T exclude this>(obj:T, getParent:Func<T>)
Dirk Boer
  • 8,522
  • 13
  • 63
  • 111

1 Answers1

3

One way of looking at this is that in getAllParents(obj, getParent), it's possible for getParent to return something wider than its input type, and that it's this wider type you want to see as an output. The this type in TypeScript is essentially "the current class type whatever it happens to be" (which is implemented as an "implicit" generic type parameter). But you wanted Category, the type returned by x => x.parent.

So we can rewrite getAllParents to explicitly account for this; obj is of type T, and getParent accepts a T, but it returns a U which is wider than T. Which means T should be constrained as T extends U:

export function getAllParents<T extends U, U>(
    obj: T, getParent: (x: T) => U) {
    let result: U[] = [];

    let parent = getParent(obj);

    while (parent != null) {
        result.push(parent);

        parent = getParent(obj);
    }

    return result;
}

That compiles cleanly, and now your usage looks like:

parents = () => {
    return getAllParents(this, x => x.parent);
    //function getAllParents<this, Category>(obj: this, getParent: (x: this) => Category): Category[]        
}

so T is inferred as this, and U is inferred as Category, and the output is Category[] as desired.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    great and extended answer! thanks! – Dirk Boer Aug 23 '23 at 14:57
  • aii, wait I put an error in my code - the second is getParent(parent) - that doesn't compile for me, but if I cast it to `as T` it works - might be a bit ugly, but for me it works! – Dirk Boer Aug 23 '23 at 15:02