sdk
sdk copied to clipboard
Callable class generic type not inferred from context
Dart does not infer the generic type of a callable class from the context, even when the type is unambiguous. For example:
class Validator<T> {
String? call(T? value) => null;
}
TextFormField(validator: Validator()); // T inferred as dynamic (dart 3.10.1)
- TextFormField.validator expects String? Function(String?).
- Declaring Validator<String>() works, but is redundant, because any other type produces a compile-time error.
- In theory, the compiler could infer T = String automatically, since it’s the only valid choice.
- Dart already does this inference for closures, but not for callable classes.
This would make callable classes more ergonomic in situations like this.
Is this behavior intended? Why? Could Dart support inferring the generic type of a callable class from the context?
Yes, it's intended.
Maybe we can do better, but the current inference specification doesn't.
The context type of String? Function(String?) does not constrain the type parameter of Validator() by itself. Downwards inference does not look at the type of call of the type being created. It doesn't know that it should.
Coercion from callable object to function happens after type inference of Validator(), at which point it has been inferred as Validator<dynamic>().
To do better, downwards inference would need to be coercion aware:
- If the context type of an expression is a function type, and the uninstantiated type being compared to that function type has a
callmethod, try doing downwards inference against the function type of thecallmethod instead, then lift the result back as type arguments to the expression. - Which gets even more complicated if it also has to consider implicit instantiation of a generic function, if the
callmethod is generic and the context type isn't.
@stereotype441
Could this limitation be considered as a feature request?
Allowing type inference for callable classes would make the use of generics more ergonomic and reduce redundancies like Validator<String>() in scenarios where the type is already clear.
This could be a valuable improvement for Dart in future versions. 🙂
@arthurbcd, your example does not indicate that there's an urgent need to define Validator as shown. If your design allows for a different placement of the type parameter, you can move the choice of T such that it will be inferred as String:
class TextFormField {
TextFormField({required String? Function(String?) validator}) {
print(validator.runtimeType);
validator('');
}
}
class Validator {
String? call<T>(T? value) {
print(T);
return null;
}
}
void main() => TextFormField(validator: Validator());
The point is that Validator() is inferred with no changes (the class is now non-generic, so there are no missing type arguments to compute). Next, it is coerced to Validator().call because the context type String? Function(String?) is a function type, and the static type of Validator() (that's Validator, too) has a call method. Finally, it is instantiated as Validator().call<String> because of implicit function instantiation on the generic function denoted by Validator().call, again based on the context type.
You can try other types like String? Function(Pattern?) or String? Function(Object?) in order to see that this generic function instantiation step is actually guided by the parameter type of the TextFormField constructor.
It's worth noting that this gives rise to a warning: "Implicit tear-off of the call method". This warning is emitted because this is a feature which is on a short-list of features that might be discontinued. The ability of a class instance with a method named call to masquerade as a function object is incomplete, and it is a non-goal for the language team to make it complete.
If it is helpful (or even necessary) for you to rely on having a call method and treating the enclosing instance as a function (as far as it goes), then by all means continue to do so. However, if possible, switching to use a real function is a more future-safe choice.
In the meantime, using a generic call method rather than a non-generic call method on a generic class could help you getting the desired behavior.