Search code examples
dartgenericstypescastingtype-conversion

Why can't a generic factory redirect to a specific subclass?


Below is some code that (currently) doesn't compile:

sealed class MyValue<ValueT> {
  final ValueT value;

  const MyValue(this.value);

  const factory MyValue.bool(bool value) = MyBool;
  const factory MyValue.num(num value) = MyNumber;
}

final class MyNumber extends MyValue<num> {
  const MyNumber(super.value);
}

final class MyBool extends MyValue<bool> {
  const MyBool(super.value);
}

Dart complains that "The return type 'MyBool/MyNum' of the redirected constructor isn't a subtype of 'MyValue'. Try redirecting to a different constructor."

However, I don't understand why. I feel like it should be perfectly feasible for const MyValue.bool(true) to be a valid MyBool constant.

This doesn't have anything to do with constants, though, as the same problem happens with non-const factories:

sealed class MyValue<ValueT> {
  final ValueT value;

  const MyValue(this.value);

  factory MyValue.bool(bool value) => MyBool(value);
  factory MyValue.num(num value) => MyNumber(value);
}

final class MyNumber extends MyValue<num> {
  const MyNumber(super.value);
}

final class MyBool extends MyValue<bool> {
  const MyBool(super.value);
}

Now, I did see this question which does seem related to the problem I am having, except in my situation I'm not trying to return different types depending on the input. I also wasn't satisfied by the answer, since something like

sealed class MyValue<ValueT> {
  final ValueT value;

  const MyValue(this.value);

  factory MyValue.bool(bool value) => (MyBool(value) as MyValue<ValueT>);
  factory MyValue.num(num value) => (MyNumber(value) as MyValue<ValueT>);
}

final class MyNumber extends MyValue<num> {
  const MyNumber(super.value);
}

final class MyBool extends MyValue<bool> {
  const MyBool(super.value);
}

not only compiles, but works completely fine at runtime. The problem is, having the explicit as cast seems redundant, and I also still can't make const instances...

Could someone explain to me why the type system doesn't work the way I expect here? (I'm assuming that I'm missing something here and there is a specific design choice that leads to this outcome I am seeing.) It's also worth mentioning that what I'm really trying to do is make a sort of "type-union", something where I can exhaustively switch over the types of "my values" (in this case, num and bool) -- so if somebody could tell me of another way to accomplish this, that'd work just as well too. Thanks in advance!


Solution

  • The rules for forwarding constructors is that the constructor they forward to must return a subtype of the type the constructor is called on, for any type variables supplied to the superclass.

    If you write:

    abstract class Super<T> {
      T get value;
      factory Super.bool(bool b) = SubBool;
    }
    class SubBool implements Super<bool> {
      final bool value;
      SubBool(this.value);
    }
    

    then you have the problem that someone can write:

      Super<String>.bool(true);
    

    That cannot be allowed, because the static type of that is Super<String>, and the value it would create is a SubBool which implements Super<bool>, which is-not-a Super<String>.

    Rather than trying to prevent that in some cases, and allow it in other cases, you're simply disallowed from writing that forwarding constructor.

    The forwarding constructor must create a Super<T>, and a SubBool is-not-a Super<T> in general.

    As for the

    not only compiles, but works completely fine at runtime.

    that's because you haven't tried to write MyValue<String>.bool(true).

    What you can do instead of factory constructors is static functions:

    import "dart:core";
    import "dart:core" as core;
    abstract class Super<T> {
      T get value;
      
      static Super<core.bool> bool(core.bool b) => SubBool(b);
    }
    

    (The use of the name bool as a method, conflicting with the type bool, complicates everything. Otherwise it would be a simple static function.)