Given such classes
sealed class Base { /* ... */ }
class X extends Base { /* ... */ }
class Y extends Base { /* ... */ }
class Foo<T extends Base> {
final T t;
const Foo({required this.t});
}
Reading the docs it seems impossible to restrict the type of an instance of Foo<T> so that T must be a real sub types of Base and may not be Base. Is it possible?
Background is that I want to switch over a Foo<T> instance:
final a = Foo<X>(/* ... */);
final result = switch (a) {
Foo<X> _ => 'x',
Foo<Y> _ => 'y',
Foo<Base> _ => throw 'does not exist'; // <--
}
The missing_enum_constant_in_switch rule requires me to specify the Base _ case. That is bad, because it matches all cases, so that if we later add a new class Z extends Base {/* ... */}, the rule would no longer report a missing case but just use the Foo<Base> case and throw does not exist.
Given that Base is sealed it also is abstract and hence cannot be instantiated and hence I would expect the compiler to not require that case - but I understand that it does, because in general the type restriction allows for Foo<Base>.
As a workaround I am currently switching over a.t.
final a = Foo<X>(/* ... */);
final result = switch (a.t) {
X _ => 'x',
Y _ => 'y',
// no `Base` case is required.
}
This looks fine in an example, but is very cumbersome and error prone in realistic production code, because it would require a manual unchecked cast of a if downstream functions depend on a rather than on a.t:
final a = Foo<X>(/* ... */);
final result = switch (a.t) {
// THIS IS NOT POSSIBLE
X _ => somethingX(a as Foo<X>),
Y _ => somethingY(a as Foo<Y>),
}
This would not work, because a is of type Foo<Base> and hence flutter throws type 'Foo<Base>' is not a subtype of type 'Foo<X>' in type cast.
Hence, we further must workaround by changing somethingX and somethingY to also accept the downcasted value like this:
final a = Foo<X>(/* ... */);
final t = a.t;
final result = switch (t) {
X _ => somethingX(a, t),
Y _ => somethingY(a, t),
}
or by doing something like:
final a = Foo<X>(/* ... */);
final t = a.t;
final result = switch (t) {
X _ => somethingX(a.cast<X>()),
Y _ => somethingY(a.cast<Y>()),
}
where cast is:
Foo<T extends Base> {
// ...
Foo<T2> cast<T2 extends Base>() => Foo(
t: t as T2,
);
}
It's not possible.
Dart type parameters are only restricted by subtype checks, so there is no way to accept both
XandYand also not acceptBase, not with the current definitions.The one hack you'd probably try, and which won't actually work, is to have a hidden shared superclass:
Then you'd think that someone outside of your library would not be able to create a
Foo<_SecretBase>, and they can't useBase, so would have to use one of the proper subtypes ofBase. Sadly it doesn't work, since a raw type likenew Foo()would "instantiate to bounds" and create aFoo<_SecretBase>, even if the caller couldn't write the type.So, it's not possible to restrict type arguments to proper subtypes of a shared supertype.