-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Inconsistent Generic Type Inference Leads to Runtime Type Error #60742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This is a well-known issue. You might want to take a look at dart-lang/language#524, which is a request for statically checked variance (please vote for that issue if you wish to help promoting this feature). With the feature, you can mark the classes that give rise to the run-time failure, and turn the dangerous typing situation into a compile-time error: class KLineChartData<inout T> { ... }
class KLineChart<inout T> { ... } You'd need to enable the experiment 'variance' in order to allow those With this, you'll see that the return statement in In the given example it is actually sufficient to change this return type, and the example runs without errors. The underlying issue is that it is unsafe to use a covariant type variable in a non-covariant position in the signature of a member. See dart-lang/language#296 and dart-lang/language#297 for more info on this particular typing situation. In short, this means that the declaration of Unfortunately, we can't just make it safe, for that we'd need the 'variance' feature. However, you can enable the lint 'unsafe_variance' in order to get a heads-up if you ever declare such a member again (or someone else does ;-). So the first advice is simply to avoid declaring members that are flagged by this lint. However, there are several other ways to deal with this kind of issue, apart from simply avoiding the non-covariantly typed member declarations. Take a look at the many issues linked from dart-lang/language#524 in order to see much more about this topic. In particular, you can maintain a special discipline in the usage of non-covariantly typed members: Make them private and make sure that they are never accessed on any other receiver than class KLineChartData<T> {
const KLineChartData({
required this.items,
required KChartItemToValueCallback<T> itemToValue,
}) : _itemToValue = itemToValue;
final List<T> items;
final KChartItemToValueCallback<T> _itemToValue;
double itemToValue(T item) => _itemToValue(item);
} With this, you won't get the run-time failure at the time where you are calling Another thing you can do is to emulate the statically checked invariance manually. This can be done by introducing an otherwise unused type parameter and a type alias which enforces that the extra type parameter is used in a specific manner (at least outside the library where it is declared): import 'dart:developer';
typedef _Inv<X> = X Function(X);
typedef KLineChartData<X> = _KLineChartData<X, _Inv<X>>;
class _KLineChartData<T, Invariance extends _Inv<T>> {
const _KLineChartData({required this.items, required this.itemToValue});
final List<T> items;
final KChartItemToValueCallback<T> itemToValue;
}
typedef KChartItemToValueCallback<T> = double Function(T item);
typedef KLineChart<X> = _KLineChart<X, _Inv<X>>;
class _KLineChart<T, Invariance extends _Inv<T>> {
final KLineChartData<T> data;
const _KLineChart({required this.data});
void print() {
final values = data.items.map((item) => data.itemToValue(item)).toList();
inspect(values);
}
}
// In some other library.
class Helper {
Helper._();
// It's now a compile-time error to omit the type argument in the return type.
static KLineChart<int> create() {
final data = [1, 2, 3, 4, 5, 6, 7];
final chartData = KLineChartData(
items: data,
itemToValue: (item) => item.toDouble(),
);
return KLineChart(data: chartData);
}
}
void main() {
final chart = Helper.create();
chart.print();
} This technique will faithfully emulate the typing properties of making the type parameters of |
To be pedantic, that is not generic type inference, which usually tries to find the best type arguments for something based on hints from the context or contents. There is no inference here, the "raw" type is simply has its blanks filled in by instantiating to bounds. Generally, if you write a raw generic type as a type, aka. a "type annotation", there is no generic inference. When a raw identifier denoting a generic type is used as a type, like writing (And it's a know annoyance, I'd love to try to infer type parameters for all raw types, if there is anything to infer them from. It's just not always possible, and would sometimes break existing code which depends on the The quickest fix here would be: static KLineChart<int> create() { |
To me, it is surprising it works fine when a |
That's surely because the local variable had a better declared type than In general, the context type of an expression plays an important role for the type inference performed on that expression. void main() {
List<Object> xs = [1, 2, 3];
xs.add("Hello!"); // OK, it's a `List<Object>`.
List<int> ys = [1, 2, 3]; // .. or this: `var ys = [1, 2, 3];`, which works the same.
List<Object> zs = ys; // Yes, because of the dynamically checked variance, this is allowed.
zs.forEach((x) => print(x)); // No problems.
zs.add("Hello"); // No compile-time error, but throws at run time.
} So the difference between returning something directly and returning a local variable which is initialized to the "same" value is that the former will have the declared return type of the enclosing function as its context type, and the latter will have the type of the variable (and that variable might not declare a type at all, so the expression is inferred with no constraints from the outside). |
I'll close this issue because the topic has been unfolded in detail before. Thanks for reporting it, it's useful to confirm that it matters! |
Description
When executing the following Dart code:
We get the following runtime error:
Root Cause
The issue is due to generic type inference. In the
create()
method, no explicit generic type is provided forKLineChart
, so Dart infersdynamic
:As a result, the type of
itemToValue
becomesdouble Function(dynamic)
at runtime, which leads to a type mismatch with the actual(int) => double
function.Inconsistent Behavior Observed
Interestingly, the code behaves correctly in the following variations, even though the type is still not explicitly declared at the function signature:
1. Assigning to a local variable before returning:
2. Explicitly specifying the generic type at construction:
Expected Behavior
The compiler should either:
dynamic
that later causes a function signature mismatch, orWhy This Is a Problem
This behavior introduces a dangerous inconsistency:
Suggested Fix or Feature
Improve the compiler's ability to:
Dart Version (on macOS 15.5 24F74 darwin-arm64)
The text was updated successfully, but these errors were encountered: