Description
This is the tracking issue for introducing a statically checked mechanism for declaration-site variance in Dart.
Background: The original request motivating this kind of feature is dart-lang/sdk#213; the initial proposal for declaration-site invariance is dart-lang/sdk#213. The initial proposal for the related feature known as use-site invariance is dart-lang/sdk#229, and the corresponding tracking issue is dart-lang/sdk#753.
Note that this issue does come up in practice. Here are a few examples gathered since April 2023. Many of them contain interesting discussions!
- Inconsistent Generic Type Inference Leads to Runtime Type Error sdk#60742
- Crash type casting issues in Dart related to generic values and null checks, type '(String?) => void' is not a subtype of type '((dynamic) => void)?' sdk#60288
- Runtime Error on Inferred Type For Closure In List with
orElse
sdk#60009 - Missing function parameter type detection. #4230
- Avoid contravariant function fields sdk#57799
- type '(int) => void' is not a subtype of type '(Object) => void' sdk#57052
- generic functions throw TypeError once generic information is lost sdk#56231
- dart vm can't upcasting generic type of function getter sdk#55427
- List.toList() Type problem sdk#55321
- Dart infers "num" for result of generic function when only ints are involved sdk#55315
- Unnecessary Cast detected when removing it would cause an error. sdk#54439
- Unsound static type in kernel AST when function type is returned sdk#54200
- Access to parent generic type sdk#54122
- Wildcard type bounds sdk#54108
- Issue with generic function definition ((String?) => Future<String>' is not a subtype of type '((dynamic) => FutureOr<String?>)?') sdk#52168
- Generic type in not inferred when used outside of class that is generalized sdk#52249
- Weird
not a subtype of
issue sdk#52826 - Downcasting generic-types crashes with an Unhandled type exception sdk#52943
- Unwanted
Object
-inference induced type errors. #3156 - void type can be used as an generic sdk#53179 (comment)
- Declared type is never respected using extensions in dart 3. sdk#53481
- [Type Error]
(int) => void
is not a subtype of type(dynamic) => void
sdk#53523 - Give users more help in solving contravariance issues sdk#33697
- Calling Generic Type Anonymous Functions #3380
- Detect the following situation during compile time - not during runtime #3385
- mergeSort fails when static type of a list does not match a runtime type core#663
The text below describes properties of this feature which are good candidates for being adopted. Many things can still change, and a full feature specification will be written and used to manage the discussions about the final design.
Variance in Dart Today
As of Dart 2.4 or earlier, every type variable declared for a generic class is considered covariant. The core meaning of this is that a parameterized type C<T2>
is a subtype of C<T1>
whenever T2
is a subtype of T1
. Other subtype rules can then be used to show subtype relationships like List<int> <: Iterable<dynamic>
and Map<String, String> <: Map<Object, Object> <: dynamic
.
This type rule is not sound; that is, in order to maintain heap soundness it is necessary to check certain types dynamically. This means that a program with no compile-time errors can fail with a type error at run time.
For instance, with the declaration List<num> xs
and some expression e
with static type num
, it is necessary to check during evaluation of xs.add(e)
that the value of e
actually has the type which is required by xs
: It is possible that it is a List<int>
or even a List<Never>
, and it would then be a dynamic type error if the value of e
is a double
, even though the expression had no type errors at compile-time.
Dynamically checked covariance enables many software designs that would be rejected by a traditional statically checked approach to variance (e.g., as in Java or C#). This allows developers to make a trade-off between more flexible types (e.g., a variable of type List<num>
is allowed to refer to a List<int>
) in return for accepting the potential dynamic type errors (a List<int>
will work safely under the type List<num>
in a lot of ways, just not all).
We want to enable a statically checked typing discipline for variance as well (rejecting more programs, but providing a compile-time guarantee against the run-time type errors described above). This feature is concerned with the provision of support for that.
Declaration-site Variance
Declaration-site variance can be used to declare a strict and statically checked treatment of variance for each type variable of a generic class.
Syntactically, declaration-site variance consists in allowing each type parameter declaration of a generic class declaration to include one of the following modifiers: out
, in
, or inout
. We say that such a type parameter has explicit variance.
The use of type parameters with explicit variance in the body of the enclosing class is restricted. It is a compile-time error for a type variable marked out
to occur in a non-covariant position in the signature of a member declaration; and for a type variable marked in
to occur in a non-contravariant position. For example:
abstract class Good<out X, in Y, inout Z> {
X get m1;
void set m2(Y value);
Z m3<U extends Z>(List<Z> zs);
}
class Bad<out X, in Y> {
Y get m1; // Error.
void set m2(X value); // Error.
Y m3<U1 extends X, U2 extends Y>(List<X> xs); // Error.
}
Here are some core properties of declaration-site variance:
We obtain the following subtype relationships: Let C
be a generic class with one type parameter X
. Assume that S
is a subtype of T
. If X
is marked out
then C<S> <: C<T>
; if X
is marked in
then C<T> <: C<S>
. Note that there is no subtype relationship between C<S>
and C<T>
if X
is marked inout
, unless S == T
.
A type parameter with explicit variance can be used in the specification of a superinterface. For example:
class A<out X, in Y, Z> {
X get m;
}
class B<out X, inout Y, in Z> implements A<X, Y, Z> {}
Soundness is ensured via a number of rules like the following: It is a compile-time error if a type parameter X
marked out
occurs in a non-covariant position in an actual type argument for a superinterface D
when the corresponding type parameter of D
is marked out
; and if X
occurs in a non-contravariant position in an actual type argument for D
when the corresponding type parameter is marked in
; and if X
occurs at all in an actual type argument for D
when the corresponding type parameter is marked inout
.
The interaction with dynamically checked covariant type parameters is similarly guarded: It is a compile-time error if a type parameter X
marked out
occurs in a non-covariant position in an actual type argument for a superinterface D
when the corresponding type parameter of D
has no explicit variance.
In return for all these restrictions, we get static safety: For a class where all type parameters have explicit variance, every (non-dynamic) member access which is statically type correct is also dynamically safe (no type checks on parameter types etc. are needed at run time).
In general, declaration-site variance can be used for classes which are intended to be strictly type checked with respect to variance everywhere.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status