skip to Main Content

I have a Freezed generated class with two factory constructors, both containing the same field that is a generic. Given a list of this class I would like to be able to filter based on the type that the field is.

Here is my class:

@freezed
sealed class Artifact<T extends Location> with _$Artifact<T> {
  const Artifact._();

  const factory Artifact.FromServer(
      {required String id,
      @_LocationConverter() required T location
}) = ServerArtifact<T>;

  const factory Artifact.NewArtifact({
    @_LocationConverter() required T location,
  }) = NewArtifact<T>;

  factory Artifact.fromJson(Map<String, Object?> json) =>
      ServerArtifact.fromJson(json);
}

My generic type Location is a Freezed union that can either be a point or a polygon.

With this class I can create new Artifacts like:

final newArtifact = Artifact.NewArtifact(
    location: Location.FromPoint(
      latitude: curLocation.latitude,
      longitude: curLocation.longitude,
    ),
  );

The problem with creating artifacts like this, is when I debugged my application it looks like Dart is storing the type of newArtifact as _$NewArtifactImpl<Location> — even if I’m passing in a Location.fromPoint() (which returns as PointLocation class that extends Location). So if I add newArtifact to a list I am unable to filter using the whereType<Artifact<PointLocation>>(). To get around this I made my own custom filter as shown below:

List<Artifact<PointLocation>> get pointArtifacts => artifacts
    .where((a) => a.location is PointLocation)
    .map<Artifact<PointLocation>>((a) => a as Artifact<PointLocation>)
    .toList();

However, when this runs I get the error:

type ‘_$NewArtifactImpl’ is not a subtype of type ‘Artifact’ in type cast

I would not only like to filter to get artifacts with a PointLocation, but also be able to be able to narrow the type of Artifact to Artifact

2

Answers


  1. Chosen as BEST ANSWER

    Instead of creating an Artifact by using the typical Freezed factory constructor without explicitly setting generics I was able to create my artifact as follows:

    final newArtifact = Artifact<PointLocation>.NewArtifact(
      title: title,
      description: description,
      location: PointLocation(
        latitude: curLocation.latitude,
        longitude: curLocation.longitude,
      ),
    );
    

    Which then allowed me to use the whereType array filtering.


  2. You have a generic Freezed class Artifact<T> where T extends Location, and you are trying to filter a list of Artifact instances by the specific type of Location they hold, e.g., PointLocation.

    Unlike Java, Dart does retain type information for generics at runtime due to its reified generics.

    It means your issue with filtering and casting Freezed classes does not come from Dart’s handling of generics, but rather from how Dart’s type system handles downcasting generic types.
    When you attempt to (down)cast Artifact<Location> to Artifact<PointLocation>, the Dart’s type system cannot guarantee the type safety of this operation at compile time without explicit checks or conversions.

    type '_$NewArtifactImpl<Location>' is not a subtype of type 'Artifact<PointLocation>' in type cast
    

    See "Dart / Type system / Generic type assignment":

    In the context of your Artifact<Location> and Artifact<PointLocation> scenario, Dart’s type system allows you to assign a subtype to a supertype variable (covariance), mirroring the List<MaineCoon> to List<Cat> example from the documentation. That means you can assign a more specific list (List<MaineCoon>) to a broader list type (List<Cat>) without explicit casting.

    https://dart.dev/assets/img/language/type-hierarchy-generics.png

    However, assigning a supertype to a subtype variable (the reverse direction, analogous to assigning List<Animal> to List<Cat>) is not allowed by Dart’s static analysis because it introduces the possibility of runtime type errors.

    That is what is meant by "implicit downcast," which Dart disallows for non-dynamic types to make sure type safety. The documentation confirms that to perform such an assignment, an explicit cast is required (myAnimals as List<Cat>), though this cast can fail at runtime if myAnimals does not actually contain Cat instances.

    In the case of your Freezed class Artifact<T>, when trying to downcast from Artifact<Location> to Artifact<PointLocation>, Dart’s type system treats this similarly to the List<Animal> to List<Cat> casting scenario. Even though Dart’s generics are reified and retain type information at runtime, allowing for runtime type checks (e.g., using is checks), the static type system prevents implicit downcasting to make sure type safety.

    That means that while you can check at runtime if an Artifact‘s location is a PointLocation using is, statically assigning Artifact<Location> to Artifact<PointLocation> without an explicit cast is disallowed because it could lead to type errors at runtime if the actual location is not a PointLocation.


    You might need to focus on safely handling the instances based on their actual runtime types, as Dart allows for runtime type checking of generic types. For filtering and transforming a list of Artifact<Location> to Artifact<PointLocation>, consider using runtime checks for both the artifact and its location, while avoiding unsafe downcasting:

    List<Artifact<PointLocation>> get pointArtifacts {
      return artifacts.where((artifact) => artifact.location is PointLocation).map((artifact) {
        // That check is safe due to Dart's reified generics, making sure `location` is indeed a PointLocation.
        final PointLocation pointLocation = artifact.location as PointLocation;
        // Use a factory constructor or a method to safely convert or recreate
        // the artifact with a PointLocation, making sure type safety.
        return Artifact<PointLocation>.fromServer(
          id: artifact.id,
          location: pointLocation,
        );
      }).toList();
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search