I encounter a problem with type exception while writing a dart class with generic type for modular flutter package. I tried to reproduce it with normal dart program. I'm not sure if it's possible to do what I tried to do. I struggled with it for like many hours. The code is as below.
https://dartpad.dev/ec27dbb96381e85906db89d258bc5a4c
void main() {
var sc = Shelter<Cat>(
name: 'test shelter',
data: [Cat(id: 'cat-1', name: 'cat no.1')],
extractParam: (data) => data.id,
);
var sd = Shelter<Dog>(
name: 'test shelter dog',
data: [Dog(dogId: 'dog-1', name: 'dog no.1')],
extractParam: (data) => data.dogId,
);
print(sc.extractParam(Cat(id: 'cat-2', name: 'cat no.2')));
print(sd.extractParam(Dog(dogId: 'dog-2', name: 'dog no.2')));
var dog = Dog(dogId: 'dog-3', name: 'dog no.3');
var test = Test(
data: dog,
shelter: sd,
);
test.printTest();
print("hello before testtest()");
for (var c in sc.data) {
final d = sc.extractParam(c);
print(d);
}
testtest(sd);
}
void testtest(Shelter shelter) {
for (var a in shelter.data) {
final b = shelter.extractParam(a);
print(b);
}
}
class Test<T> {
final T data;
final Shelter<T> shelter;
const Test({
required this.data,
required this.shelter,
});
void printTest() => print('This is test: ${shelter.extractParam(data)}');
}
class Dog {
final String dogId;
final String name;
const Dog({
required this.dogId,
required this.name,
});
}
class Cat {
final String id;
final String name;
const Cat({
required this.id,
required this.name,
});
}
class Shelter<T> {
final String name;
final List<T> data;
final String Function(T data) extractParam;
const Shelter({
required this.name,
required this.data,
required this.extractParam,
});
}
console output:
ā test_dart dart ./bin/test_generic_field_extract.dart
cat-2
dog-2
This is test: dog-3
hello before testtest()
cat-1
Unhandled exception:
type '(Dog) => String' is not a subtype of type '(dynamic) => String'
#0 testtest (file:///Users/tharineemunkwamdee/projects/test_dart/bin/test_generic_field_extract.dart:34:23)
#1 main (file:///Users/tharineemunkwamdee/projects/test_dart/bin/test_generic_field_extract.dart:29:3)
#2 _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:296:19)
#3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189:12)
ā test_dart
PS. the program can compile without error but crash on runtime
The problem is the signature of the testtest
function:
void testtest(Shelter shelter) {
for (var a in shelter.data) {
final b = shelter.extractParam(a);
print(b);
}
}
The type of extractParam
is directly dependent on the generic type T
of the Shelter
. But when you declare a parameter or variable type of Shelter
without providing a type parameter, Dart has no choice but to default to the most permissive type the generic constraints will allow. In this case where there are no constraints, Shelter
will be functionally equivalent to Shelter<dynamic>
.
When it comes to arbitrary data from fields and method return values, there's still a fair amount you can do when the generic type is dynamic
because any type will be compatible with dynamic
. But when you start to nest the generic type within another type like, say, a function type, Dart gets a lot less flexible with what you can do. I've gone into more detail on why this is, but the short explanation is that int
and dynamic
are compatible types, but List<int>
and List<dynamic>
are not, and it's because generics are not beholden to the same rules of inheritance as normal types are.
The interesting thing about this particular case is that the Dart runtime is having a disagreement over what the type of shelter.extractParam
should be. The object itself knows that it's a Shelter<Dog>
, and, by extension, shelter.extractParam
is a (Dog) => String
, which can be confirmed by calling print(shelter.runtimeType)
within testtest
. However, when the program is actually running, the parameter definition is saying that shelter
is a Shelter<dynamic>
, which means that shelter.extractParam
should be a (dynamic) => String
. And because (Dog) => String
and (dynamic) => String
are incompatible types, the runtime will freak out when it accesses shelter.extractParam
expecting one type and it finds a different type altogether.
At any rate, you essentially have two options to get around this. The first is the more obvious one, which is to put a generic type parameter on testtest
, which will allow callers to implicitly (or explicitly) provide a concrete type to what kind of Shelter
the function is dealing with:
void testtest<T>(Shelter<T> shelter) {
for (var a in shelter.data) {
final b = shelter.extractParam(a);
print(b);
}
}
The other option is less advisable, but depending on what you're trying to do, it might be your only real option. You can explicitly cast shelter
inside testtest
to dynamic
to disable the runtime's type checking entirely:
void testtest(Shelter shelter) {
for (var a in shelter.data) {
final b = (shelter as dynamic).extractParam(a);
print(b);
}
}