Description
I have a class like this:
abstract class Foo<T extends Object> {
List<T> read();
void write(List<T> value);
}
And some subclasses that specify a concrete type parameter:
class StringFoo extends Foo<String> {
@override List<String> read() => ['hello', 'world'];
@override void write(List<String> values) {
print('writing strings $values');
}
}
class IntFoo extends Foo<int> {
@override List<int> read() => [1,2,3];
@override void write(List<int> values) {
print('writing ints $values');
}
}
And some top level functions that operate on Foo<T>
s:
void processFoo<T extends Object>(Foo<T> f) {
final data = f.read();
// do some processing...
f.write(data);
}
This works well in places where processFoo
is called with a concrete type:
void main() {
processFoo(IntFoo());
}
There are other places where I want to call processFoo
with an instance of Foo
whose concrete type is not known at compile time. For example:
Foo fooByName(String name) {
switch (name) {
case 's': return StringFoo();
case 'i': return IntFoo();
default: throw 'bad name $name';
}
}
void processFooByName(String name) {
final foo = fooByName(name);
processFoo(foo);
}
This also seems to work:
void main() {
processFooByName('i');
}
Months later I made what I thought was a minor refactor to processFoo
void processFoo<T extends Object>(Foo<T> f) {
final data = f.read();
// do some processing...
f.write([for (final (i, x) in data.indexed) if (i % 2 == 0) x]);
}
processFoo(IntFoo());
still works fine, but now processFooByName('i');
throws a runtime type error: type 'List<Object>' is not a subtype of type 'List<int>' of 'values'
.
I understand that fooByName
has no choice but to return a Foo<Object>
, and I understand that the runtime typecheck is required because Dart's class generics are unsafely covariant, but I can't seem to figure out how to implement processFooByName
so that it doesn't throw, other than by inlining fooByName
like this:
void processFooByName(String name) {
switch (name) {
case 's': return processFoo(StringFoo());
case 'i': return processFoo(IntFoo());
default: throw 'bad name $name';
}
}
Is there any other way to preserve Foo
's type argument in a "type-erased" context?