Description
This issue is a proposal to change the built-in class Type
such that it accepts a single type parameter with no bound, and to change instances of Type
that reify a Dart type, such that they satisfy t is Type<T>
whenever t
reifies T
.
Note that this feature has been mentioned in several different discussions, e.g., in connection with abstract static methods and virtual static methods, #356. This issue ensures that we have a specific location where the concept is explicitly proposed.
Motivation
The main motivation for this feature is that it allows for an improved static analysis of reified types.
In particular, we could express something which is similar to static extensions (if we allow int.foo()
to call an extension method on a receiver of type Type<int>
, otherwise we'd need to use (int).foo()
):
extension IntStatics on Type<int> {
int parseHex(String source, {int onError(String source)?}) =>
int.parse(source, radix: 16, onError: onError);
}
extension AnyStatics<X> on Type<X> {
bool isTypeOf(Object? o) => o is X;
}
void main() {
var i = int.parseHex("0x1F");
print(String.isTypeOf(1)); // 'false'.
}
Next, it would be possible to determine statically that a type variable satisfies a bound which is stronger than the declared one. For example, we could allow the following:
class A<X> {
void foo(X x1, X x2) {
if (X is Type<int>) { // This could promote `X` to `X extends int`.
// `x1` and `x2` would then have type `X & int`.
print(x1.isEven == x2.isEven); // OK.
}
}
}
Another example usage is dependency injection, that is, factories associated with class hierarchies:
abstract class Animal {}
abstract class Herbivore implements Animal {}
abstract class Carnivore implements Animal {}
class Cow implements Herbivore { toString() => "cow"; }
class Cat implements Carnivore { toString() => "cat"; }
class Human implements Herbivore, Carnivore { toString() => "human"; }
extension AnimalFactory<X extends Animal> on Type<X> {
static var _factories = {
// For abstract classes, deliver a default.
Animal: Cat.new,
Herbivore: Cow.new,
Carnivore: Cat.new,
// Concrete classes, deliver exactly the requested type.
Cow: Cow.new,
Cat: Cat.new,
Human: Human.new,
};
X create() => _factories[this]!() as X;
}
// The precise value of the type variable need not be known statically.
X doCreate<X extends Animal>() => X.create();
void main() {
print(Herbivore.create()); // 'cow'.
print(Cat.create()); // 'cat'.
Human human = doCreate();
print(human); // 'human'.
}
Static Analysis
A type literal t
which is statically known to be a reification of a type T
is statically known to have type Type<T>
. Otherwise, Type<T>
is treated just like any other generic type.
An expression of the form X is Type<T>
where X
is a type variable would promote X
to X extends T
in the true continuation, in the case where T
is a subtype of the bound of X
.
Dynamic Semantics
A reified type t
that reifies the type T
has a run time type such that t is Type<S>
is true if and only if T <: S
.