Search code examples
flutterdartgenericsnullabletypechecking

Dart: How to check the type of a sometimes nullable generic type?


Given the following dart 3 snippet, is it possible to match against A being nullable in the switch clause, and to find out which kind of nullable A is? (String?, int? etc) The type checker incorrectly assumes that A can never be nullable, and I end up with a case that I can't seem to match against.

void main() {
  String? x = test(null);
  String y = test("hi");
}

A test<A extends Object?>(A a) {
  switch((A,a.runtimeType)){
    case (String,Null) :
      print("found miscasted nullable string");
    case (String,_) : 
      print("found non-null string: $a");
    // This case should match, but something is weird.
    case (String?,_) :
      print("found nullable string");
    case (var x,Null):
      print("found something null: $x $a");
      if(x is String?)
        print("$x is String?");
      else
        print("$x is not String?");
    case (_,_):
      print("failed matching completely" ); 
  };
  return a;
}

The output becomes

found something null: String? null
    String? is not String?
found non-null string: hi

and I can't seem to find a way to match the input type against String?.

I need this to work in Flutter, so dart:mirror can't be a solution.

Edit: Thanks to the answer from @jamesdlin, I now have the following functions in my code:

Type typeOf<A>() => A;
bool isSomeKindOf<T,S>() => S == typeOf<T>() || S ==  typeOf<T?>();

. This means that now I can do things like

bool isIntOrNullableInt<A>(A a) => isSomeKindOf<int,A>();
print(isIntOrNullableInt(3)); // true
print(isIntOrNullableInt(null as Int?)); // true
print(isIntOrNullableInt("3")); // false

which means that I can now do stuff like

A getOrDefault<A>(String token, A defaultVal) =>
    (
    isSomeKindOf<int,A>() ? source.getInt(token) ?? defaultVal :
    isSomeKindOf<String,A>() ? source.getString(token) ?? defaultVal :
    defaultVal
    ) as A; 

which works whether A is a Foo or a Foo?, which was my secret agenda all along.
This lets me finally have the following working:

String prop1 = getOrDefault("prop1", "property missing");
int? prop2 = getOrDefault("prop2", null);

Solution

  • Note the message from the analyzer (with emphasis added):

    The null-check pattern will have no effect because the matched type isn't nullable.

    String? in your pattern isn't checking A == String?, it's treated as a null-check pattern which a separate kind of pattern, and that pattern is applied to String.

    That's also why the analyzer complains about case (String?, _) being redundant with case (String, _):

    This case is covered by the previous cases.

    Also note that your check if (x is String?) won't work either because x was matched to A, and A is B checks if A is an instance of B. String is not an instance of a String?; String (and String?) are instances of the Type class.

    I'm not sure if you can do what you want with patterns, but you instead could do:

    void main() {
      String? x = test(null);
      String y = test("hi");
    }
    
    Type identityType<T>() => T;
    
    A test<A extends Object?>(A a) {
      if (A == String) {
        if (a == null) {
          // Note that this is not possible.
          print("found miscasted nullable string");
        } else {
          print("found non-null string: $a");
        }
    
        // `A == String?` is not legal syntax.
      } else if (A == identityType<String?>()) {
        print("found nullable string");
      } else if (a == null) {
        print("found something null: $A $a");
    
        if (A == identityType<String?>()) {
          // This is not possible since it would have been caught earlier.
          print("$A is String?");
        } else {
          print("$A is not String?");
        }
      } else {
        print("failed matching completely");
      }
      return a;
    }