Skip to content

Type parameter is not preserved in type-erased context #54624

Closed
@Mike278

Description

@Mike278

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-languageDart language related items (some items might be better tracked at github.com/dart-lang/language).type-questionA question about expected behavior or functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions