Dart - Casting List<SuperType> to List<SubType> using generics

2,643

In Dart (and indeed in many languages) generics screws with the concept of inheritance. You would think that if Bar inherits from Foo, that List<Bar> would also be castable to List<Foo>.

This is not actually going to be the case because of how generics work. When you have a generic class, every time you use that class with a different type, that type is treated as a completely separate class. This is because when the compiler compiles those types, class MyGenericType<Foo> extends BaseClass and class MyGenericType<Bar> extends BaseClass are basically converted to something like class MyGenericType_Foo extends BaseClass and class MyGenericType_Bar extends BaseClass.

Do you see the problem? MyGenericType_Foo and MyGenericType_Bar are not descendants of one another. They are siblings of each other, both extending from BaseClass. This is why when you try to convert a List<Entity> to List<Vehicle>, the cast doesn't work because they are sibling types, not a supertype and subtype.

With all this being said, while you cannot directly cast one generic type to another based on the relationship of the generic type parameter, in the case of List there is a way to convert one List type to another: the cast method.

List<Entity> entityList = <Entity>[...];
List<Vehicle> vehicleList = entityList.cast<Vehicle>(); // This cast will work

One thing to note though, if you are casting from a supertype generic to a sub-type generic and not all the elements of the list are that new type, this cast will throw an error.

Share:
2,643
michael_vendler
Author by

michael_vendler

Updated on December 18, 2022

Comments

  • michael_vendler
    michael_vendler over 1 year

    I am new to Flutter and Dart, coming from native Android.

    Android has a very nice database abstraction architecture called the Room Persistence Library. As far as I am aware, no such database abstraction architecture exists for Flutter using the MVVM / MVC design patterns.

    My solution was to create a Dart version of it myself. I got it pretty much done after a few headaches, but I cannot seem to get LiveData to work properly using generics.

    I set up my class like this:

    class LiveData<T> {
      ...
    }
    

    Now when I want to return some data, it can either be an Object or List<Object>. I found a neat hack for differentiating the two from T:

    ...
    
    // Parse response
    // This checks if the type is an instance of a single entity or a list.
    if (entity is T) {
      cachedData = rawData.isEmpty ? null : entity.fromMap(rawData.first) as T;
    } else {
      cachedData = rawData.map((e) => entity.fromMap(e)).toList() as T;
    }
    
    ...
    

    The problem lies in the second block:

    cachedData = rawData.map((e) => entity.fromMap(e)).toList() as T;
    

    With the error:

     - Unhandled Exception: type 'List<Entity>' is not a subtype of type 'List<Vehicle>' in type cast
    

    The question then becomes: How can I cast Entity to Vehicle when I do not have access to the Vehicle class. Only an instance of it is assigned to an Entity entity variable.

    Here's a snippet to demonstrate my access to Vehicle:

    final Entity entity;
    
    ...assign Vehicle instance to entity...
    
    print(entity is Vehicle)  // True
    

    I've tried using .runtimeType to no avail. I have also thought about splitting LiveData into two classes, the second one being LiveDataList. Although this seems to be the easiest solution to not bug the code- it would bug me (bad pun is intentional) and break the otherwise pretty direct port of Room.



    As a temporary solution, I have abstracted out the build logic into a generic function to be passed to the LiveData in the constructor.

    final T Function(List<Map<String, dynamic>> rawData) builder;
    

    And now I call that instead of the previous code to build the cachedData.

    // Parse response
    cachedData = builder(rawData);
    

    With the constructor for the LiveData<List<Vehicle>> called when accessing all vehicles in the Dao<Vehicle> being:

    class VehicleDao implements Dao<Vehicle> {
      ...
    
      static LiveData<List<Vehicle>> get() {
        return LiveData<List<Vehicle>>(
          ...
          (rawData) => rawData.map((e) => Vehicle.fromMap(e)).toList(),
          ...
        );
      }
    }
    
  • michael_vendler
    michael_vendler about 4 years
    Thank you for the reply! I don't think I was clear enough with my question. Because LiveData is generic, it lives inside another package and is designed to be independent of any subtypes. This means I do not have access to Vehicle in the LiveData class to cast it with.
  • michael_vendler
    michael_vendler about 4 years
    I found a temporary solution by abstracting out the build logic into final T Function(List<Map<String, dynamic>> rawData) builder that is passed into the constructor of the LiveData allowing for any builder that returns T. This is not a solution, but more of a workaround. I will keep testing with this to see how I like it.
  • Bobby
    Bobby almost 4 years
    In my case, I got an error when using reduce on a List that derived (derived, derived) is not a subtype of parent (parent, parent). By using the cast approach on the list that reduce is invoked on, it works.