angulartypescripttypesoverriding

Overriding parent class function with generic type causes "Property 'x' in type 'y' is not assignable to the same property in base type 'Parent'."


I have this code:

class BaseModel {
  protected getAttribute<T extends BaseModel>(): keyof T | null {
    return null;
  }
}

class Payment extends BaseModel {}
class Item extends BaseModel {}

class Sale extends BaseModel {
  value?: number;
  payment?: Payment;
  items?: Item[];

  protected override getAttribute(): keyof Sale | null {
    return 'payment';
  }
}

This is a simplified version of the real code. getAttribute does some other things, but solving this simple implementation will solve my problem. I need to be able to override the function getAttribute in the parent class BaseModel with the child's own implementation of it. The function needs to return only properties present in Sale (or any other child of BaseModel), but I'm getting this error in getAttribute in Sale:

Property 'getAttribute' in type 'Sale' is not assignable to the same property in base type 'BaseModel'.
Type '() => keyof Sale | null' is not assignable to type '<T extends BaseModel>() => keyof T | null'.
Type 'keyof Sale | null' is not assignable to type 'keyof T | null'.
Type 'string' is not assignable to type 'keyof T'.
Type 'string' is not assignable to type 'never'.

I tried changing getAttribute in Sale to:

protected override getAttribute<T extends Sale>(): keyof T | null {
     return 'payment'; 
}  

but it also didn't work. Sale is a class that extends BaseModel, so Sale should be equivalent to T, shouldn't it?


Solution

  • The problem with

    class BaseModel {
      protected getAttribute<T extends BaseModel>(): keyof T | null {
        return null;
      }
    }
    

    is that getAttribute() is a generic method/function, where the type parameter T is attached to the call signature. As such, it gets specified along with the function parameters, at the call site. The person who writes getAttribute() can't choose T; it's the person who calls getAttribute(). So BaseModel is class where its getAttribute() method will accept any T constrained to BaseModel the caller wants, and will return keyof T or null. The fact that you are trying to override getAttribute() in subclasses with something that doesn't work that way implies that you haven't got your generic in the right place.


    Instead, conceptually, you want getAttribute() to not itself be a generic method. It's the BaseModel class itself that should be generic. So more like BaseModel<T>.getAttribute(), and less like BaseModel.getAttribute<T>(). That means BaseModel is no longer a specific type. If we make that change, moving the T extends BaseModel from getAttribute() to the class declaration, and replace every BaseModel with BaseModel<T> for an appropriate T, it looks like this:

    class BaseModel<T extends BaseModel<T>> {
      protected getAttribute(): keyof T | null {
        return null;
      }
    }
    
    class Payment extends BaseModel<Payment> { }
    class Item extends BaseModel<Item> { }
    
    class Sale extends BaseModel<Sale> {
      value?: number;
      payment?: Payment;
      items?: Item[];
    
      protected override getAttribute(): keyof Sale | null {
        return 'payment';
      }
    }
    

    This works just fine because now getAttribute() can only return keyof T for the T defined in the containing class. For Payment that's Payment (hence Payment extends BaseModel<Payment>), for Item that's Item, and for Sale that's Sale.

    So that addresses the question as asked. But:


    That's a curious type parameter declaration, isn't it? We've got T extends BaseModel<T>, where T is constrained by a function of itself. Another way to say this is that T is bounded by F<T> for some type function F. Or, T is an "F-bounded" generic.

    And if you have an F-bounded generic of the form interface Foo<T extends Foo<T>> {⋯} or type Bar<T extends Bar<T>> = ⋯; or class Baz<T extends Baz<T>> {⋯}, you can often (but not always) avoid the generic by instead using the polymorphic this type. The this type represents the "current" type, which automatically narrows in subtypes. Inside BaseModel, this is (a subtype of) BaseModel. But inside Payment, this is (a subtype of) Payment. Essentially this is implicitly an F-bounded generic. That suggests we make the following change:

    class BaseModel {
      protected getAttribute(): keyof this | null {
        return null;
      }
    }
    
    class Payment extends BaseModel { }
    class Item extends BaseModel { }
    
    class Sale extends BaseModel {
      value?: number;
      payment?: Payment;
      items?: Item[];
    
      protected override getAttribute(): keyof Sale | null {
        return 'payment';
      }
    }
    

    The explicit generic is just gone, and getAttribute() returns keyof this | null. And luckily, it all compiles without error. This may or may not end up working out for your use case. With T extends BaseModel<T> you can always decide when to stop inheriting the genericness, whereas this is always this and gets narrower and narrower with inheritance. With keyof this it's easy enough, but if you tried to return this you'd find it a little trickier. I'm not going to digress into exactly when and how this will work for you. I'll just say: it's reasonable to try it, always remembering you can fall back to an explicit F-bounded generic if you must.

    Playground link to code