The Dart type system

The Dart language is type safe: it uses a combination of static type checking andruntime checks toensure that a variable’s value always matches the variable’s static type,sometimes referred to as sound typing.Although types are mandatory, type annotations are optionalbecause of type inference.

For a full introduction to the Dart language, including types, see thelanguage tour.

One benefit of static type checking is the ability to find bugsat compile time using Dart’s static analyzer.

You can fix most static analysis errors by adding type annotations to genericclasses. The most common generic classes are the collection typesList<T> and Map<K,V>.

For example, in the following code the printInts() function prints an integer list,and main() creates a list and passes it to printInts().

  1. void printInts(List<int> a) => print(a);
  2.  
  3. void main() {
  4. var list = [];
  5. list.add(1);
  6. list.add("2");
  7. printInts(list);
  8. }

The preceding code results in a type error on list (highlightedabove) at the call of printInts(list):

  1. error The argument type 'List' can't be assigned to the parameter type 'List<int>' • argument_type_not_assignable

The error highlights an unsound implicit cast from List<dynamic> to List<int>.The list variable has static type List<dynamic>. This is because theinitializing declaration var list = [] doesn’t provide the analyzer withenough information for it to infer a type argument more specific than dynamic.The printInts() function expects a parameter of type List<int>,causing a mismatch of types.

When adding a type annotation (<int>) on creation of the list(highlighted below) the analyzer complains that a string argument can’t be assigned toan int parameter. Removing the quotes in list.add("2") resultsin code that passes static analysis and runs with no errors or warnings.

  1. void printInts(List<int> a) => print(a);
  2.  
  3. void main() {
  4. var list = <int>[];
  5. list.add(1);
  6. list.add(2);
  7. printInts(list);
  8. }

Try it in DartPad.

What is soundness?

Soundness is about ensuring your program can’t get into certaininvalid states. A sound type system means you can never get intoa state where an expression evaluates to a value that doesn’t matchthe expression’s static type. For example, if an expression’s statictype is String, at runtime you are guaranteed to only get a stringwhen you evaluate it.

Dart’s type system, like the type systems in Java and C#, is sound. Itenforces that soundness using a combination of static checking(compile-time errors) and runtime checks. For example, assigning a Stringto int is a compile-time error. Casting an Object to a string usingas String fails with a runtime error if the object isn’t astring.

The benefits of soundness

A sound type system has several benefits:

  • Revealing type-related bugs at compile time.A sound type system forces code to be unambiguous about its types,so type-related bugs that might be tricky to find at runtime arerevealed at compile time.

  • More readable code.Code is easier to read because you can rely on a value actually havingthe specified type. In sound Dart, types can’t lie.

  • More maintainable code.With a sound type system, when you change one piece of code, thetype system can warn you about the other piecesof code that just broke.

  • Better ahead of time (AOT) compilation.While AOT compilation is possible without types, the generatedcode is much less efficient.

Tips for passing static analysis

Most of the rules for static types are easy to understand.Here are some of the less obvious rules:

  • Use sound return types when overriding methods.
  • Use sound parameter types when overriding methods.
  • Don’t use a dynamic list as a typed list.

Let’s see these rules in detail, with examples that use the followingtype hierarchy:

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

Use sound return types when overriding methods

The return type of a method in a subclass must be the same type or asubtype of the return type of the method in the superclass. Considerthe getter method in the Animal class:

  1. class Animal {
  2. void chase(Animal a) { ... }
  3. Animal get parent => ...
  4. }

The parent getter method returns an Animal. In the HoneyBadger subclass,you can replace the getter’s return type with HoneyBadger (or any other subtypeof Animal), but an unrelated type is not allowed.

  1. class HoneyBadger extends Animal {
  2. void chase(Animal a) { ... }
  3. HoneyBadger get parent => ...
  4. }
  1. class HoneyBadger extends Animal {
  2. void chase(Animal a) { ... }
  3. Root get parent => ...
  4. }

Use sound parameter types when overriding methods

The parameter of an overridden method must have either the same typeor a supertype of the corresponding parameter in the superclass.Don’t “tighten” the parameter type by replacing the type with asubtype of the original parameter.

Note: If you have a valid reason to use a subtype, you can use the covariant keyword.

Consider the chase(Animal) method for the Animal class:

  1. class Animal {
  2. void chase(Animal a) { ... }
  3. Animal get parent => ...
  4. }

The chase() method takes an Animal. A HoneyBadger chases anything.It’s OK to override the chase() method to take anything (Object).

  1. class HoneyBadger extends Animal {
  2. void chase(Object a) { ... }
  3. Animal get parent => ...
  4. }

The following code tightens the parameter on the chase() methodfrom Animal to Mouse, a subclass of Animal.

  1. class Mouse extends Animal {...}
  2.  
  3. class Cat extends Animal {
  4. void chase(Mouse x) { ... }
  5. }

This code is not type safe because it would then be possible to definea cat and send it after an alligator:

  1. Animal a = Cat();
  2. a.chase(Alligator()); // Not type safe or feline safe

Don’t use a dynamic list as a typed list

A dynamic list is good when you want to have a list withdifferent kinds of things in it. However, you can’t use adynamic list as a typed list.

This rule also applies to instances of generic types.

The following code creates a dynamic list of Dog, and assigns it toa list of type Cat, which generates an error during static analysis.

  1. class Cat extends Animal { ... }
  2.  
  3. class Dog extends Animal { ... }
  4.  
  5. void main() {
  6. List<Cat> foo = <dynamic>[Dog()]; // Error
  7. List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
  8. }

Runtime checks

Runtime checks in tools like the Dart VM and dartdevcdeal with type safety issues that the analyzer can’t catch.

For example, the following code throws an exception at runtime because it is an errorto assign a list of Dogs to a list of Cats:

  1. void main() {
  2. List<Animal> animals = [Dog()];
  3. List<Cat> cats = animals;
  4. }

Type inference

The analyzer can infer types for fields, methods, local variables,and most generic type arguments.When the analyzer doesn’t have enough information to infera specific type, it uses the dynamic type.

Here’s an example of how type inference works with generics.In this example, a variable named arguments holds a map thatpairs string keys with values of various types.

If you explicitly type the variable, you might write this:

  1. Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};

Alternatively, you can use var and let Dart infer the type:

  1. var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>

The map literal infers its type from its entries,and then the variable infers its type from the map literal’s type.In this map, the keys are both strings, but the values have differenttypes (String and int, which have the upper bound Object).So the map literal has the type Map<String, Object>,and so does the arguments variable.

Field and method inference

A field or method that has no specified type and that overridesa field or method from the superclass, inherits the type of thesuperclass method or field.

A field that does not have a declared or inherited type but that is declaredwith an initial value, gets an inferred type based on the initial value.

Static field inference

Static fields and variables get their types inferred from theirinitializer. Note that inference fails if it encounters a cycle(that is, inferring a type for the variable depends on knowing thetype of that variable).

Local variable inference

Local variable types are inferred from their initializer, if any.Subsequent assignments are not taken into account.This may mean that too precise a type may be inferred.If so, you can add a type annotation.

  1. var x = 3; // x is inferred as an int
  2. x = 4.0;
  1. num y = 3; // a num can be double or int
  2. y = 4.0;

Type argument inference

Type arguments to constructor calls andgeneric method invocations areinferred based on a combination of downward information from the contextof occurrence, and upward information from the arguments to the constructoror generic method. If inference is not doing what you want or expect,you can always explicitly specify the type arguments.

  1. // Inferred as if you wrote <int>[].
  2. List<int> listOfInt = [];
  3.  
  4. // Inferred as if you wrote <double>[3.0].
  5. var listOfDouble = [3.0];
  6.  
  7. // Inferred as Iterable<int>
  8. var ints = listOfDouble.map((x) => x.toInt());

In the last example, x is inferred as double using downward information.The return type of the closure is inferred as int using upward information.Dart uses this return type as upward information when inferring the map()method’s type argument: <int>.

Substituting types

When you override a method, you are replacing something of one type (in theold method) with something that might have a new type (in the new method).Similarly, when you pass an argument to a function,you are replacing something that has one type (a parameterwith a declared type) with something that has another type(the actual argument). When can you replace something thathas one type with something that has a subtype or a supertype?

When substituting types, it helps to think in terms 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’stype with a subtype.

Let’s look at examples of simple type assignment and assignment withgeneric types.

Simple type assignment

When assigning objects to objects, when can you replace a type with adifferent type? The answer depends on whether the object is a consumeror a producer.

Consider the following type hierarchy:

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

Consider the following simple assignment where Cat c is a consumer and Cat()is a producer:

  1. Cat c = Cat();

In a consuming position, it’s safe to replace something that consumes aspecific type (Cat) with something that consumes anything (Animal),so replacing Cat c with Animal c is allowed, because Animal isa supertype of Cat.

  1. Animal c = Cat();

But replacing Cat c with MaineCoon c breaks type safety, because thesuperclass may provide a type of Cat with different behaviors, suchas Lion:

  1. MaineCoon c = Cat();

In a producing position, it’s safe to replace something that produces atype (Cat) with a more specific type (MaineCoon). So, the followingis allowed:

  1. Cat c = MaineCoon();

Generic type assignment

Are the rules the same for generic types? Yes. Consider the hierarchyof lists of animals—a List of Cat is a subtype of a List ofAnimal, and a supertype of a List of MaineCoon:

List<animal> -> List<cat> -> List<mainecoon>

In the following example, you can assign a MaineCoon list to myCats becauseList<MaineCoon> is a subtype of List<Cat>:

  1. List<Cat> myCats = List<MaineCoon>();

What about going in the other direction? Can you assign an Animal list to a List<Cat>?

  1. List<Cat> myCats = List<Animal>();

This assignment passes static analysis,but it creates an implicit cast. It is equivalent to:

  1. List<Cat> myCats = List<Animal>() as List<Cat>;

The code may fail at runtime. You can disallow implicit castsby specifying implicit-casts: false in the analysis options file.

Methods

When overriding a method, the producer and consumer rules still apply.For example:

Animal class showing the chase method as the consumer and the parent getter as the producer

For a consumer (such as the chase(Animal) method), you can replacethe parameter type with a supertype. For a producer (such asthe parent getter method), you can replace the return type witha subtype.

For more information, seeUse sound return types when overriding methodsand Use sound parameter types when overriding methods.

Other resources

The following resources have further information on sound Dart: