Dart Type inference
The task of interpreting the data types of fields, methods, local variables and most generic type arguments is handled by the static analyser. However, when not enough information is provided to the static analyser it assumes it to be of dynamic type.
Consider the following example that explains the concept of type inference working with generics. Here, a variable ‘ variable ’ holds a map that contains a map of key and value pairs.
Map< String, dynamic > variable = { ' var1 ' : ' Type ', ' var2 ' : 10 } ;
As an alternate option, we can use ‘var’ or ‘final’ instead of ‘Map’ leaving the task of inferring the type of the compiler.
var variable = { ' var1 ' : ' Type ', ' var2 ' : 10 } ; // This works similar to Map< String, Object >
The ‘ Map ’ literal interprets its type from the type of key and value pairs specified in it. The variable then interprets its own type from the type of map literal. For example, in the above example, the map has keys that are both of type ‘ String ’ while the value pairs have different types ( String and int ). So, the ‘ Map ’ literal has the type Map< String, Object >, and so does the arguments variable.
Field and method type inference
A property or function of the class that does not have a specific data type and that overrides the property or function of the superclass, inherits the data types as same as that of the superclass method.
While, a field is not declared with some specific type or inherits a type, the data type is interpreted based on the initial value assigned to it.
Static field type inference
The data types of static fields and variables are interpreted from their initializer. An important point to remember is that the inference fails if it comes across any kind of cycle ( that is, if inferring a type for the variable depends on knowing the type of that variable ).
Local variables inference
The data types of local variables are inferred from their initializer, if any. Subsequent assignments are not taken into account that means type may be inferred very precisely. If so, you can add a type annotation.
var x = 5 ; // x is inferred as an integer
x = 5.0 ;
num y = 7 ; // Here, y can inferred as integer or double
y = 7.0 ;
Type argument inference
The data type of arguments passed to constructor calls and generic method invocations are interpreted based on a combination of information. The downward information from the context of occurrence, and upward information from the arguments to the constructor or generic method. In case the compiler does not interpret the data type correctly, it is advisable to explicitly mention the data types of the argument.
// This is interpreted as < int >[ ]
List< int > int_list = [ ] ;
// This is interpreted as < double >[ 3.0 ]
var double_list = [ 5.4 ] ;
// This is interpreted as an Iterable< int >.
var int_iterable = listOfDouble.map( ( x ) => x.toInt( ) ) ;
In the above example, ‘ x ’ is interpreted as of ‘ double ’ data type using downward information. With the help of upward information, the return type of the closure is inferred as ‘ int ’. Dart uses this return type as upward information when inferring the map( ) method’s type argument : < int >.
Substituting types
When we override a method, we re-write it or replace it with something of one type with something of a new type with some difference. Similarly, when an argument is passed to a function, we are replacing the parameter of some declared type with some another type. This could be easily understood by an example of consumers and producers. A consumer absorbs a type, and a producer generates a type. You can replace a consumer’s type with a supertype and a producer’s type with a subtype.
Let’s look at examples of simple type assignments and assignments with generic types.
Simple type assignment
Consider the following type hierarchy:
This is a hierarchy of animals where the supertype is ‘Animal ’ and the subtypes are ‘ Monkey ’, ‘ Cat ’ and ‘ Cow ’. ‘Cat ’ has the subtypes of ‘ Lion ’ and ‘ Tiger ’.
Consider the following simple assignment where the consumer is Cat c and Cat( ) is a producer :
Cat c = Cat( ) ;
Try to understand it this way that it’s alright to replace something that consumes a specific type like here, ‘ Cat ’ with something that consumes anything which kind of contains it like here ‘ Animal ’. Therefore, replacing ‘ Cat c ’ with ‘ Animal c ’ is allowed, because ‘ Animal ’ is a supertype of ‘ Cat ’.
Animal c = Cat( ) ;
But replacing ‘ Cat c ’ with ‘ Lion c ’ breaks type safety, because the superclass may provide a type of Cat with different behaviours, such as Tiger :
Lion c = Cat( ) ;
In a producing position, it’s safe to replace something that produces a type ‘ Cat ’ with a more specific type ‘ Lion ’. So, the following is allowed:
Cat c = Lion( ) ;
Generic type assignment
We can follow the same rules for the generic data types as well. Consider the hierarchy of lists of animals—a List of Cat is a subtype of a List of Animal, and a supertype of a List of Lion :
List< Animal > -> List< Cat > -> List< Lion >
In the following example, Lion list can be assigned to myCats because List< Lion > is a subtype of List< Cat > :
List< Cat > myCats = < Lion > [ ] ;
What do you think can the reverse be possible? Can you assign an Animal list to a List< Cat > ?
List< Cat > myCats = < Animal >[ ] ;
This assignment doesn’t pass static analysis because it creates an implicit downcast, which is disallowed from non-dynamic types such as Animal.
Version note : The packages belonging to language versions before 2.12 ( when support for null safety was introduced ), code downcast from these non-dynamic types implicitly. We can disallow non-dynamic downcasts in a pre-2.12 project by specifying implicit-casts: false in the analysis options file.
To make this code pass static analysis, use an explicit cast, which might fail at runtime.
List< Cat > myCats = < Animal >[ ] as List< Cat > ;
Methods
Remember that the producer and consumer rules apply even when we override a method. For example consider the following piece of code :
The class ‘ Animal ’ contains the ‘ chase ’ method as the consumer and the parent function ‘ getter ’ as the producer.
For a consumer the ‘ chase( Animal ) ’ method, we can replace the parameter type with a supertype. For a producer the parent ‘ getter( ) ’ method, we can replace the return type with a subtype.
For more information, include Use sound return types when overriding methods and Use sound parameter types when overriding methods.