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?
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.