Callback with generic type parameter in Dart

skreborn picture skreborn · Apr 25, 2019 · Viewed 7.1k times · Source

I'm trying to define a callback function that needs to accept a generic parameter and return a value of the same type. Keep in mind that the following example is an oversimplified version of what I actually need.

final T Function<T>(T value) self = (value) => value

This results in the following error, that I can't seem to get rid of.

The argument type '(dynamic) → dynamic' can't be assigned to the parameter type '<T>(T) → T'

dart(argument_type_not_assignable)

The only thing that seems to work is to give the value a type, but that defeats the purpose of using a type parameter in the first place.

final T Function<T>(T value) same = <String>(value) => value;

I need it to be generic so that the caller can pass the type it expects in return. I also need it to be stored in a variable, so I can pass it around as a callback function.

If this isn't directly possible, do you know of any workarounds? Thank you in advance.


Here's a more complete example if the requirements aren't clear.

abstract class Provider<T> {
  T get bar;
}

class StringProvider extends Provider<String> {
  String get bar => 'bar';
}

class NumberProvider extends Provider<int> {
  int get bar => 42;
}

class Foo {
  final T Function<T>(Provider<T> provider) provide;

  const Foo({ this.provide });
}

test() {
  final foo = Foo(provide: (provider) => provider.bar);

  String strValue = foo.provide(StringProvider()); // should be 'bar'
  int numValue = foo.provide(NumberProvider()); // should be 42
}

The annoying thing is that Dart actually understands that foo.provide(StringProvider()) will return a string and that using NumberProvider will indeed return an integer, and yet, the error is still risen for the line where the variable is actually given a value.

final foo = Foo(provide: (provider) => provider.bar);
The argument type '(dynamic) → dynamic' can't be assigned to the parameter type '<T>(Provider<T>) → T'

Answer

skreborn picture skreborn · Apr 25, 2019

It turns out that I can cheat the type checker by giving any concrete type when defining the value. Note that dynamic is not allowed, but anything else goes.

final foo = Foo(provide: <int>(provider) => provider.bar);

This both gets rid of the error and allows for the provide method to return the correct type when called.

In conclusion, this seems like a simple shortcoming of the type checker, not something that's actually impossible or difficult to achieve using the already existing language features. I will be raising an issue on the language's GitHub repository to allow for further investigation and discussion.


Update #1: the issue has been opened on GitHub.


Update #2: the issue has been resolved, and it turns out that the behavior is by design. Quoting Erik Ernst from the SDK team:

Try this: final foo = Foo(provide: <T>(Provider<T> provider) => provider.bar);!

The problem is that you're passing a non-generic function to the Foo constructor, and you should pass a generic function. There is no subtype relationship between a generic function type and a non-generic function type, so according to the type checker you might as well pass a String, and that's the reason for the 'can't be assigned to' message.

It turns out that simply adding <T> before the parameter list (instead of int as in the original workaround) solved the issue.

final foo = Foo(provide: <T>(provider) => provider.bar);

This forces Dart to understand that provider is of type Provider<T> and that the method will return a value of type T, saving us from using a concrete type and still getting rid of the error.