language
language copied to clipboard
Allow type-checking type variables
I often see people trying to check the type of a type variable by doing if (T is int) ...
.
This doesn't work, the expression T
evaluates to a Type
object which is not of type int
.
The idea has merit, though.
We could perhaps special case typeVariable is type
to actually check whether the type of typeVariable
is a subtype of type
. The expression is useless otherwise, so it's not like we will break any reasonable program, and users obviously find the syntax intuitive, since they keep using it.
If we made the Type
type generic, then you could write if (T is Type<int>)
with no special casing. Not very intuitive though.
Making Type
generic might be the less intuitive approach for subtype checking via is
, but it opens up other possibilities. For instance, code generators often use a List<Type>
somewhere in an annotation, but really need each type to be a subtype of some interface. Being able to use a List<Type<Interface>>
could express this more accurately. It would also help users spot errors easier and could improve auto-complete (since only appropriate subtypes would be suggested).
This happens fairly often with annotation processors in Java, and I think it would be nice to have a similar feature in Dart. Speaking of similarities to Java, I'd like to add something to the Type<T>
suggestion: For an expression e
with a static type T
, I think it's appropriate for e.runtimeType
to have static type of Type<T>
(similar to how getClass()
behaves in Java).
One issue with making Type
objects generic is that if we get "open type"/type deconstruction functionality, which would let you get the T
of a List<T>
as a type variable, then you would also be able to reify a Type<T>
object's type as a type variable.
That's a significant cost to ahead-of-time compilers. They currently know that the only types which can occur as a type variable are the types that are introduced as a type argument. Only those types need to be retained at run-time in a way that supports being a real type.
If you can do doWith(Object x) { if (x.runtimeType is Type<var T>) { ... use T ... }}
then all types need to be retained. That's a significant extra overhead.
I often see people trying to check the type of a type variable by doing
if (T is int) ...
.
We could also add a lint to prevent this case.
We could also add a lint to prevent this case.
However if (T is num)
could be valid of num
, ìnt
, and double
. if (T == num)
is only valid for num
.
then all types need to be retained. That's a significant extra overhead.
@lrhn This would not be a problem if runtimeType
returned a Type<dynamic>
, right? Type literals could still have a parameterized static type, they have to evaluate to a real type either way for subtype checking to work. Not changing runtimeType
behavior would also be less breaking, since it can be overridden.
Of course, we might as well mention that the test which is the topic of this issue can be expressed today, if we're willing to pay for an allocation of a list (or we could use some other generic class, if that's cheaper):
void foo<X>() {
if (<X>[] is List<int>) {
// Here it is guaranteed that `X <: int`.
}
}
void main() {
foo<String>();
}
But we should definitely try to find the time to get support for a generic Type
, such that X is Type<T>
iff X <: T
.
I would assume that an instance of Type<dynamic>
represents the type dynamic
, so it would be surprising if 2.runtimeType
returned Type<dynamic>
instead of Type<int>
.
If we actually have a Type<dynamic>
representing the type int
, then we also lose most of the advantage of having the type parameter on Type
. There would be no way to trust the Type
type parameter, so you can't actually use it for anything important.
It's also a detour and a waste of effort to turn a type parameter into a Type
object in order to check what type it refers to. The Type
object is unnecessary if we introduced a type ordering operator, say <:
so you could just ask if (T <: num) ...
. No Type
object in sight, and none needed. And we avoid having to worry about Type
objects, which are generally useless anyway.
(Or we could decide that is
can do double duty as both instance check and subtype check when the LHS is a type expression, and we are back at the originally proposed T is num
).
Surely, 2.runtimeType
should return the reification of int
, and that object should have a type that implements Type<int>
(this essentially means that it would have the type argument int
at the type Type
, e.g., because it's an instance of BuiltInVmType<int>
and class BuiltInVmType<X> implements Type<X> {...}
).
In other words, if we make Type
generic then each reified type should definitely have a type argument which is maximally informative.
It's also a detour and a waste of effort
Agreed, but nothing stops a compiler from compiling if (X is Type<T>) ...
into if (X <: T) ...
where the subtype operator <:
would be supported directly in the kernel language. So we shouldn't have to worry too much about the fact that X is Type<T>
looks expensive.
Pulling from #1971, given these:
class Constraint { int get temp => 0; }
void constrained<T extends Constraint>(T arg) {}
these two variations seem like they should work:
void foo1<T>(T arg) {
if(T is Constraint) { // always false because T is a Type
print(arg.temp);
constrained<T>(arg);
}
}
void foo2<T>(T arg) {
if(arg is Constraint) {
print(arg.temp); /// valid, since `arg` is promoted to `Constraint`
barConstrained<T>(arg); /// error, since `T` is NOT promoted to `T extends Constraint`
}
}
Seems this issue is about foo1
, but it would also be nice if foo2
worked as well -- after all, the compiler has already promoted arg
to be a Constraint
, so by extension it should promote T
as well.
We wouldn't be able to (soundly) promote T
based on a type test on the value of arg
: T
could perfectly well be Object
even though arg
is a Constraint
, and then we couldn't assume that T
is Constraint
(or a subtype of that) because we could have, say, other parameters of type T
, and we haven't tested them. But we would be able to conclude that arg
must have type Constraint
(or a subtype) if we could test and promote T
to Constraint
: We know that arg
is a T
, and now we just now a bit more about the value of T
.
So we'd really need the ability to express directly that we want to test and promote a type variable, and that's what this issue is about.
Is it possible to check if a generic type implements an interface during runtime?
abstract class MyInterface {
void someCapability();
}
class MyConcreteType implements MyInterface {
@override
void someCapability() {
throw UnimplementedError('Coming soon!');
}
}
void MyFlexibleFunction<T>(T param) {
// This condition does not currently exist or compile successfully
// But conveys what I am trying to achieve
if (T implements MyInterface) {
final MyInterface myInterface = param as MyInterface;
myInterface.someCapability();
}
}
This proposal aside, this is currently possible with
if (param is MyInterface)