Description
Example (inspired by from dart-lang/language#3337):
sealed class Result<T, E> {
const factory Result.value(T value) = ValueResult<T>._;
const factory Result.error(E error) = ErrorResult<E>._;
}
final class ValueResult<T> implements Result<T, Never> {
final T value;
const ValueResult._(this.value);
}
final class ErrorResult<E> implements Result<Never, E> {
final E error;
const ErrorResult._(this.error);
}
void main() {
test(const Result.value(1));
test(const Result.error("e"));
}
void test(Result<int, String> r, [bool b = false]){
switch (r) { // Result<int, String>' not exhausted, doesn't match 'ValueResult<dynamic>()'
case ValueResult<int> v: print(v.value);
case ErrorResult<String> e: print(e.error);
}
}
The error message given by both the analyzer and dart2js (using DartPad, master channel) is:
The type 'Result<int, String>' is not exhaustively matched by the switch cases since it doesn't match 'ValueResult<dynamic>()'.
However, ValueResult<dynamic>
is not a subtype of the matched element type of Result<int, String>
, so that type should not need to be matched.
(If I do match it, I get told to also match ErrorResult<dynamic>
.)
If I forward both type arguments to the subclasses:
sealed class Result<T, E> {
const factory Result.value(T value) = ValueResult<T, E>._;
const factory Result.error(E error) = ErrorResult<T, E>._;
}
final class ValueResult<T, E> implements Result<T, E> {
final T value;
const ValueResult._(this.value);
}
final class ErrorResult<T, E> implements Result<T, E> {
final E error;
const ErrorResult._(this.error);
}
void main() {
test(const Result.value(1));
test(const Result.error("e"));
}
void test(Result<int, String> r, [bool b = false]){
switch (r) {
case ValueResult<int, String> v: print(v.value);
case ErrorResult<int, String> e: print(e.error);
}
}
then the error goes away, so it's something about the Never
which makes the exhaustiveness algorithm think that ValueResult<int>
doesn't exhaust the ValueResult
-subtypes of Result<int, *>
.
I can see how ValueResult<int>
might only seem to match Result<int, Never>
, not all of Result<int, String>
, but it exhausts all ValueResult
s that are actually subtypes of Result<int, *>
, no matter the second operand, and exhausting ValueResult<int>
by itself should exhaust it as a subtype of Result<int, *>
too.
Maybe it's the "spaces" computation which gets confused.
In any case, saying that I should match ValueResult<dynamic>
, which is not related to the second type argument of Result
either, doesn't provoke confidence.