My goal is to make a rename
method that returns the class type itself. But this rename
is an interface to implement defined in the the base class so that the base class can use it.
void main() {
Cat cat = Cat("Whiskers");
Dog dog = Dog("Buddy");
Cat renamedCat = cat.rename("Mittens");
Dog renamedDog = dog.rename("Max");
print("Old cat name: ${cat.name}, New cat name: ${renamedCat.name}");
print("Old dog name: ${dog.name}, New dog name: ${renamedDog.name}");
}
Both of these two versions don't work.
Version 1:
abstract class Animal {
final String name;
Animal(this.name);
T copyWith<T extends Animal>({String? name});
}
class Cat extends Animal {
Cat(super.name);
@override
Cat copyWith({String? name}) {
return Cat(name ?? this.name);
}
}
class Dog extends Animal {
Dog(super.name);
@override
Dog copyWith({String? name}) {
return Dog(name ?? this.name);
}
}
extension AnimalExtension<T extends Animal> on T {
T rename(String newName) {
return copyWith(name: newName);
}
}
Gives this error:
Cat.copyWith' ('Cat Function({String? name})') isn't a valid override of 'Animal.copyWith' ('T Function<T extends Animal>({String? name})').
Version 2:
abstract class Animal {
final String name;
Animal(this.name);
Animal copyWith({String? name});
}
class Cat extends Animal {
Cat(super.name);
@override
Cat copyWith({String? name}) {
return Cat(name ?? this.name);
}
}
class Dog extends Animal {
Dog(super.name);
@override
Dog copyWith({String? name}) {
return Dog(name ?? this.name);
}
}
extension AnimalExtension<T extends Animal> on T {
T rename(String newName) {
return copyWith(name: newName);
}
}
Gives the error:
A value of type 'Animal' can't be returned from the method 'rename' because it has a return type of 'T
There is a solution that works, but I don't like, it smells so much, because it is bad programming:
return copyWith(name: newName) as T; // this just forces it to work
The problem is that not parsing as T is already bad programming
extension AnimalExtension<T extends Animal> on T {
T rename(String newName) {
/// T can be anything as long as its an animal,
/// you can: return Dog(newName)
/// and it will still be a valid return because dog is of type T, even if you call it from a Cat perspective
return copyWith(name: newName);
}
}
T extends Animal means that whatever it is it can transform from <E extends Animal>
to <R extends Animal>
as long as the supertype Animal is present in both (Cat and Dog for example), so as T
purpose is for the program to understands that whatever copyWith does it returns the same type that the input (if rename was calling on a Dog, the output as to be of a Dog)
the 2 best options are:
This example shows that you can have an extension for type and also that if you have an extension for the parent class Animal and the one calling it is an Animal (not a specific type, but the parent) you can call it without risk:
abstract class Animal {
final String name;
Animal(this.name);
Animal copyWith({String? name}); /// no need for T, this is not a generic class, all copyWith are already a type of the same class Animal
}
class Cat extends Animal {
Cat(super.name);
@override
Cat copyWith({String? name}) {
return Cat(name ?? this.name);
}
}
class Dog extends Animal {
Dog(super.name);
@override
Dog copyWith({String? name}) {
return Dog(name ?? this.name);
}
}
extension CatExtension on Cat {
Cat rename(String newName) {
return copyWith(name: newName);
}
}
extension DogExtension on Dog {
Dog rename(String newName) {
return copyWith(name: newName);
}
}
extension AnimalExtension on Animal {
Animal rename(String newName) {
return copyWith(name: newName);
}
}
if your object is of type Animal it will use AnimalExtension, otherwise it will use the specific extension (or just override a method as you did with copyWith)
void main() {
Animal cat = Cat("Whiskers");
Animal dog = Dog("Buddy");
Animal renamedCat = cat.rename("Mittens");
Animal renamedDog = dog.rename("Max");
print("Old cat name: ${cat.name}, New cat name: ${renamedCat.name}");
print("Old dog name: ${dog.name}, New dog name: ${renamedDog.name}");
}
The other way to fix it is by setting the bound in the beggining of the Animal class
T copyWith<T extends Animal>({String? name});
The previous method didn't bound it correctly because the class is not aware of T, only the method
abstract class Animal<T extends Animal<T>> {
final String name;
Animal(this.name);
T copyWith({String? name});
}
class Cat extends Animal<Cat> {
Cat(super.name);
@override
Cat copyWith({String? name}) {
return Cat(name ?? this.name);
}
}
class Dog extends Animal<Dog> {
Dog(super.name);
@override
Dog copyWith({String? name}) {
return Dog(name ?? this.name);
}
}
extension AnimalExtension<T extends Animal<T>> on T {
T rename(String newName) {
return copyWith(name: newName);
}
}
Now the system knows that copywith is of type Animal<T>
:
Animal<Cat>
Animal<Dog>
Animal<T>
, which uses copyWith that is the same Animal<T>
(T = Animal<Dog>
, copyWith returns Animal<Dog>
and so on)