Dart Tutorial

Dart Tutorial Single-Page Application Architecture Dart Features Dart Installation Guide Dart Basic Program Dart Syntax Dart Keywords Dart Variables Dart Comments Dart Standard Input Output Dart Important Concepts

Data Types

Built-in Data Types Numbers Strings Booleans Lists Sets Maps Runes and Graphemes Symbols Enumerations Constants Queues

Other data types

Objects Future and stream Iterable Miscellaneous types

OPERATORS

Precedence and associativity Arithmetic operators Equality and Relational operators Type Test Operators Assignment Operators Logical Operators Bitwise and Shift Operators Miscellaneous operators

Control Flow Statements

Introduction If statement If-else statement If-else-if statement Loops Switch and case Dart Break And Continue Assert In Dart

FUNCTIONS

Dart function Types of Functions Anonymous function main( ) function Lexical scope and closure Recursion Common Collection Methods

Object Oriented Concepts

Dart Object-Oriented Concepts Dart Classes Dart Constructors Dart This Keyword Dart Super Keyword Static Members Method Overriding Dart Interfaces Inheritance Dart Abstract Classes Dart Builder Classes Dart Callable Classes

Dart Type System

Dart Type System Dart Soundness Dart Type Inference

MISCELLANEOUS

Dart Isolates Dart Typedef Dart Metadata Dart Packages Dart Generics Dart Generators Dart Concurrency Dart Unit Testing Dart Html Dom Dart URIs Dart Extends, With and Implements Keywords Dart Optional Parameters Rust Vs Dart C++ vs Dart Golang Vs Dart Dart Basics Exception Handling

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:

Type inference

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 :

Type inference

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.