You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Mar 27, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Mar 27, 2026. It is now read-only.
Union of Modddels: how to properly handle narrowed types? #7
Sorry for the continuous streak of questions, I got hyped!
Using my repo from #6 as context, I now face a rather annoying issue regarding (I guess) ahead of compilation vs runtime types, when a modddel is exposed through a Riverpod provider cased Modddels in a union (in my case, the union modddels is exposed through a Riverpod AsyncNotifier, which wraps an AsyncValue<MapLayer> with MapLayer being the union of modddels: MapLayer.raster, MapLayer.vector and MapLayer.geojson).
My guess is that I’m not providing Riverpod with enough insight, but I did not manage to discover how. edit: wrong guessing 🙈
Here it is, explained by comments:
// [… imports … ]classMapextendsConsumerWidget {
constMap({Key? key}) :super(key: key);
@overrideWidgetbuild(BuildContext context, WidgetRef ref) {
final logger = ref.read(loggerProvider);
final provider =mapLayerControllerProvider(layerName:"some_raster_layer");
final rasterLayer = ref.watch(provider);
return rasterLayer.when(data: (layer) {
logger.d(layer);
// ==> layer exposed as a ValidRaster, which is expected, for it was created// with MapLayer.raster(…) with valid attributes values;// but this fails because Dart thinks it’s a MapLayer, which has no position attribute:// logger.i(layer.position.value);// ==> Hence doing a cumbersome type casting:final l = layer asValidRaster;
logger.i(l.position.value); // works fine now!returnText("OK");
}, error: (e, _) {
logger.d(e);
returnText("KO");
}, loading: () {
logger.d("Loading…");
returnText("Loading…");
});
}
}
Of course, the problem stays the same if I, say, mapValidity on layer within the widget (rather than within the controller/provider):
return rasterLayer.when(data: (layer) {
return layer.mapValidity(valid: (layer) {
logger.d(layer.runtimeType); // ValidRaster// logger.i(layer.position.value); // Error: The getter 'position' isn’t defined for the type 'ValidMapLayer'.
logger.i((layer asValidRaster).position.value); // works fine, for ValidRaster extends MapLayer// with Raster, which has the attributesreturnText("Valid layer $layer");
}, invalid: (layer) {
logger.d(layer.runtimeType); // InvalidRaster
logger.d(layer.failures); // works fine; layer is seen as an InvalidMapLayer, thoughreturnText("Invalid layer $layer");
});
}, error: (e, _) {
logger.d(e);
returnText("KO");
}, loading: () {
logger.d("Loading…");
returnText("Loading…");
});
No worries at all ! I appreciate your active participation and questions.
You (almost) never need to use type casting when using modddels. The package has been made to provide compile-time safety, and type casting breaks that.
Let me put your modddel here for reference. It's a union MapLayer with three case-modddels : Raster, Vector and Geojson.
First, creating a modddel with valid arguments doesn't mean that it will have a valid type compile-time. This is simply impossible as you can't predict or know the value of an object compile-time (unless it's a constant). What it does though is allowing you to deal with all validation states in a compile-safe way.
So for example, if you have a Raster modddel, you can do this :
voidmyFunction(Raster raster) {
// Pattern matching between the different validation states
raster.map(
valid: (valid) {
// `valid` is of type `ValidRaster`
},
invalidMid: (invalidMid) {
// `invalidMid` is of type `InvalidRasterMid` which extends `InvalidRaster`
},
);
// Or if you only need to handle the "valid" case :
raster.mapOrNull(
valid: (valid) {}, // `valid` is of type `ValidRaster`
);
}
We need pattern matching because we don't know if the raster that myFunction receives is valid or not. On the other hand, if myFunction requires receiving a ValidRaster, you can easily enforce it :
voidmyFunction(ValidRaster validRaster) { //....
So you'll need to do pattern matching somewhere before calling myFunction.
Secondly, just like for freezed, when you create an instance of a case-modddel (for example : MapLayer.raster(…) ), its type is broadened to the type of the union ( MapLayer ). That's to be expected because after all, it's a simple factory constructor. So you'll need to do "modddels pattern matching" in order to get to your Raster type from MapLayer.
voidmyOtherFunction(MapLayer mapLayer) {
mapLayer.mapMapLayer(
raster: (raster) {}, // Of type `Raster`
vector: (vector) {}, // Of type `Vector`
geojson: (geojson) {}, // Of type `Geojson`
);
// Or if you only need to handle the `Raster` case :
mapLayer.mapOrNullMapLayer(
raster: (raster) {},
);
}
Same principle applies here, you can require that myOtherFunction receives a specific case-modddel (Raster, Vector or Geojson).
In your example :
return layer.mapValidity(valid: (layer) {
logger.d(layer.runtimeType); // ValidRaster// logger.i(layer.position.value); // Error: The getter 'position' isn’t defined for the type 'ValidMapLayer'.
The reason you get an error is that in the beginning layer is of type MapLayer, and after calling mapValidity, in the valid callback, it is of type ValidMapLayer, which doesn't contain the property position because it's not a shared property. So you'll need to do some "modddels pattern matching" in order to get to ValidRaster and access the position property. A feature I personally like is that you can mix "validity" pattern matching with "modddels pattern matching" in any order, so for example you can move from MapLayer to ValidMapLayer to ValidRaster, or move from MapLayer to Raster to ValidRaster.
To get back to your specific usecase, it depends on what you need. Does your provider only hold Raster or other types of layers too ? Does it hold only valid instances ? And in your widget, do you need to handle all case-modddels / validation states ?
Depending on your answer, you can make the type of the provider exactly what you need it to be, and also handle in the widget only the needed cases (consider using maybeMap or mapOrNull if you don't need to handle all validation states for example).
Alright, you made it crystal clear what to do, and sure it unfolds naturally:
classMapextendsConsumerWidget {
constMap({Key? key}) :super(key: key);
@overrideWidgetbuild(BuildContext context, WidgetRef ref) {
final logger = ref.read(loggerProvider);
final provider =mapLayerControllerProvider(layerName:"some_raster_layer");
final someLayer = ref.watch(provider);
// TODO: load other map layers as well (as per current / user config), iterate on collection, etc.return someLayer.when(data: (mapLayer) {
// here, I first mapped over cased-modddels types, then validity, but one could do it the reverse wayreturn mapLayer.mapMapLayer(
raster: (rasterLayer) => rasterLayer.mapValidity(
valid: (validRasterLayer) =>Text(
"Valid raster layer! ${validRasterLayer.position.value}"),
invalid: (invalidRasterLayer) =>Text("Invalid raster layer! ${invalidRasterLayer.failures}")),
vector: (vectorLayer) =>Text("Vector layer! TODO: mapValidity"),
geojson: (geojsonLayer) =>Text("GeoJSON layer! TODO: mapValidity"));
}, error: (e, _) {
logger.d(e);
returnText("KO");
}, loading: () {
logger.d("Loading…");
returnText("Loading…");
});
}
}
What I was mostly missing is the existence of mapMapLayer (ie. the fact that a unioned Modddel generates such a map[ModddelName] method to map over its case-modddels, narrowing types down in the process). I know that’s properly documented, though it didn’t "click" when I read the page. Now it does, and the table mapping between Super-sealed class and Case-Modddels makes perfect sense!
Now it’s just a matter of organizing widget’s controllers, and eventually services/repositories, so as to keep the widgets light and only concerned with displaying either the expected content or a user-friendly error.
changed the title [-]Modddels + Riverpod: how to properly handle narrowed type casting?[/-][+]Union of Modddels: how to properly handle narrowed type casting?[/+]on Jun 1, 2023
changed the title [-]Union of Modddels: how to properly handle narrowed type casting?[/-][+]Union of Modddels: how to properly handle narrowed types?[/+]on Jun 1, 2023
Awesome, glad to hear you're getting the hang of it !
Now it’s just a matter of organizing widget’s controllers, and eventually services/repositories, so as to keep the widgets light and only concerned with displaying either the expected content or a user-friendly error.
That's exactly the kind of smooth usage I was aiming for with modddels. I hope you enjoy using the package, and feel free to reach out if you run into any more questions or issues.
Activity
CodingSoot commentedon Jun 1, 2023
No worries at all ! I appreciate your active participation and questions.
You (almost) never need to use type casting when using modddels. The package has been made to provide compile-time safety, and type casting breaks that.
Let me put your modddel here for reference. It's a union
MapLayerwith three case-modddels :Raster,VectorandGeojson.There are two parts to your question.
First, creating a modddel with valid arguments doesn't mean that it will have a valid type compile-time. This is simply impossible as you can't predict or know the value of an object compile-time (unless it's a constant). What it does though is allowing you to deal with all validation states in a compile-safe way.
So for example, if you have a
Rastermodddel, you can do this :We need pattern matching because we don't know if the
rasterthatmyFunctionreceives is valid or not. On the other hand, ifmyFunctionrequires receiving aValidRaster, you can easily enforce it :So you'll need to do pattern matching somewhere before calling
myFunction.Secondly, just like for freezed, when you create an instance of a case-modddel (for example :
MapLayer.raster(…)), its type is broadened to the type of the union (MapLayer). That's to be expected because after all, it's a simple factory constructor. So you'll need to do "modddels pattern matching" in order to get to yourRastertype fromMapLayer.Same principle applies here, you can require that
myOtherFunctionreceives a specific case-modddel (Raster,VectororGeojson).In your example :
The reason you get an error is that in the beginning
layeris of typeMapLayer, and after callingmapValidity, in thevalidcallback, it is of typeValidMapLayer, which doesn't contain the propertypositionbecause it's not a shared property. So you'll need to do some "modddels pattern matching" in order to get toValidRasterand access thepositionproperty. A feature I personally like is that you can mix "validity" pattern matching with "modddels pattern matching" in any order, so for example you can move fromMapLayertoValidMapLayertoValidRaster, or move fromMapLayertoRastertoValidRaster.To get back to your specific usecase, it depends on what you need. Does your provider only hold
Rasteror other types of layers too ? Does it hold only valid instances ? And in your widget, do you need to handle all case-modddels / validation states ?Depending on your answer, you can make the type of the provider exactly what you need it to be, and also handle in the widget only the needed cases (consider using
maybeMapormapOrNullif you don't need to handle all validation states for example).CodingSoot commentedon Jun 1, 2023
On this note, freezed supports directly creating a union-case and preserving its type, for example :
In modddels, this is not supported (for now). If needed, you can cast the case-modddel directly after creating it, like this :
Although it's pretty safe, it's not very clean so I'll look into implementing a better alternative.
chikamichi commentedon Jun 1, 2023
Thank you sir! I'll try that asap.
chikamichi commentedon Jun 1, 2023
Alright, you made it crystal clear what to do, and sure it unfolds naturally:
What I was mostly missing is the existence of
mapMapLayer(ie. the fact that a unioned Modddel generates such amap[ModddelName]method to map over its case-modddels, narrowing types down in the process). I know that’s properly documented, though it didn’t "click" when I read the page. Now it does, and the table mapping between Super-sealed class and Case-Modddels makes perfect sense!Now it’s just a matter of organizing widget’s controllers, and eventually services/repositories, so as to keep the widgets light and only concerned with displaying either the expected content or a user-friendly error.
Thank you again!
[-]Modddels + Riverpod: how to properly handle narrowed type casting?[/-][+]Union of Modddels: how to properly handle narrowed type casting?[/+][-]Union of Modddels: how to properly handle narrowed type casting?[/-][+]Union of Modddels: how to properly handle narrowed types?[/+]CodingSoot commentedon Jun 1, 2023
Awesome, glad to hear you're getting the hang of it !
That's exactly the kind of smooth usage I was aiming for with modddels. I hope you enjoy using the package, and feel free to reach out if you run into any more questions or issues.