Search code examples
darttypestype-inferenceswitch-expression

Why do I need an explicit downcast in an exhaustive switch expression?


Here's some dart code that seems interesting to me:

void main() {
  A<num> someA = B();
  A value = switch (someA) {
      B() => B(),
      C() => C(),
  };
}

sealed class A<T extends num> {}
final class B extends A<int> {}
final class C extends A<double> {}

If you try to compile this, you get an error about the switch statement: A value of type 'Object' can't be assigned to a variable of type 'A<num>'. Try changing the type of the variable, or casting the right-hand type to 'A<num>'.

My question is, why doesn't the compiler infer the common parent type between the two? (Well, it sort of does I guess... by defaulting to Object.) If you wrote something like var myThing = true ? 1.5 : 7;, dart infers the expected type of myThing to be num. Why doesn't it do that here?

Of course, this is all fixed when you do

void main() {
  A<num> someA = B();
  A value = switch (someA) {
      B() => B() as A,
      C() => C() as A,
  };
}

sealed class A<T extends num> {}
final class B extends A<int> {}
final class C extends A<double> {}

instead. Also, it's worth noting that this issue is not present when the type parameter <T extends num> is not present -- so there's something about generic types that is preventing the compiler from inferring the expected type (or what I expected, at least).

Edit: There is an interesting difference between switch-expression behavior and list expressions (and if you combine them):

void main() {
  // `values_var` is `List<Object>`
  var values_var            = [B(), C()];
  List<A> values_List1      = [B(), C()];
  List<A<num>> values_List2 = [B(), C()];
  // all of the above compiles fine

  A<num> someA = B();
  
  // does not compile
  A value = switch (someA) {
      B() => B(),
      C() => C(),
  };

  // DOES compile
  List<A> value = switch (someA) {
      B() => [B()],
      C() => [C()],
  };
}

This is a clearer version of my original question: Why does type inference work as I expect for the lists, but not for the switch-expression (without lists)?


Solution

  • My question is, why doesn't the compiler infer the common parent type between the two?

    This is due to the least upper bound algorithm dart uses for type inference.

    You can read about least upper bound here in the language specification:

    https://spec.dart.dev/DartLangSpecDraft.pdf#Least%20Upper%20Bounds

    In the dart language repo on github, there are a number of open issues relating to least upper bound not inferring the desired type (instead inferring an overly general type usually Object):

    https://github.com/dart-lang/language/labels/least-upper-bound

    The main issue that seems to be tracking this problem can be found here:

    https://github.com/dart-lang/language/issues/1618

    I also opened an issue relating to this myself a while ago, and the responses there are quite informative as well:

    https://github.com/dart-lang/language/issues/2953


    If you wrote something like var myThing = true ? 1.5 : 7;, dart infers the expected type of myThing to be num. Why doesn't it do that here?

    My understanding is that least upper bound only comes into play when generics are involved.

    The same class hierarchy without generics does not produce any errors.

    void main() {
      A someA = B();
      A value = switch (someA) {
          B() => B(),
          C() => C(),
      };
    }
    
    sealed class A {}
    final class B extends A {}
    final class C extends A {}
    

    Why does type inference work as I expect for the lists, but not for the switch-expression (without lists)?

    I'm not 100% certain on this, but my best guess here is that if you read the section in the language spec about list literal inference, it mentions that list literals take into account a context type when determining the type.

    If you read through the github issues linked above, it seems that the current issues surrounding least upper bounds occur when a context type is not accounted for.