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 X
and Y
and also not accept Base
, 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:
class Base {}
class _SecretBase extends Base {}
class X extends _SecretBase {}
class Y extends _SecretBase {}
class Foo<T extends _SecereteBase> { ... }
Then you'd think that someone outside of your library would not be able to create a Foo<_SecretBase>
, and they can't use Base
, so would have to use one of the proper subtypes of Base
.
Sadly it doesn't work, since a raw type like new Foo()
would "instantiate to bounds" and create a Foo<_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.