Skip to main content
The covariant keyword is a modifier that disables static enforcement of parameter contravariance during method overriding. It permits a subclass to override a method parameter with a type that is a subtype of the parameter in the superclass, effectively tightening the input requirements.

Type System Mechanics

In Dart’s sound type system, method parameters are normally contravariant. To satisfy the Liskov Substitution Principle, an overriding method must accept the same parameter type or a supertype (wider type) as the method it overrides. The covariant keyword explicitly inverts this rule for a specific parameter, allowing covariance (accepting a narrower type). This transfers type safety checks for that parameter from compile-time to runtime.

Syntax and Behavior

The keyword is placed before the parameter type in the method signature of the subclass.
class Animal {}
class Mouse extends Animal {}

class Mammal {
  // The parent method accepts any Animal
  void chase(Animal x) {}
}

class Cat extends Mammal {
  @override
  // The 'covariant' keyword allows narrowing the parameter from Animal to Mouse.
  // Without this keyword, the analyzer would flag a compile-time error.
  void chase(covariant Mouse x) {}
}

Application on Fields

When applied to an instance field, covariant applies to the implicit setter of that field. This allows a subclass to override a mutable field with a more specific type. The modifier cannot be applied to final fields because they lack setters. Additionally, overriding a getter (the read operation) with a more specific return type is inherently allowed by the type system and does not require the covariant modifier.
class Store {
  // Implicit setter accepts Object
  Object item;
  Store(this.item);
}

class IntegerStore extends Store {
  // Overrides the implicit setter to accept only int.
  // The implicit getter now returns int.
  @override
  covariant int item;
  
  // Constructor accepts int, initializes local field and superclass
  IntegerStore(int value) : item = value, super(value);
}

Runtime Implications

Using covariant bypasses static analysis, introducing the possibility of runtime exceptions. The Dart runtime injects a check to ensure the object passed matches the narrowed type. If a client treats the subclass instance as the superclass type (upcasting) and passes an argument that is valid for the superclass but invalid for the subclass, a TypeError is thrown.
void main() {
  Mammal myCat = Cat();

  // Static type is Mammal, so the analyzer allows passing any Animal.
  // However, the runtime implementation (Cat) expects a Mouse.
  
  // This throws a runtime TypeError: 
  // type 'Animal' is not a subtype of type 'Mouse' of 'x'
  myCat.chase(Animal()); 
}

Implicit Covariance in Generics

Dart generics are covariant by default. Consequently, parameters defined using a class’s generic type variable are implicitly covariant. This means that a Box<int> is a subtype of Box<Object>, even though Box<int> exposes a method accepting int while Box<Object> exposes a method accepting Object.
class Box<T> {
  // The parameter 'value' is implicitly covariant because it uses T.
  void add(T value) {}
}

void main() {
  Box<int> intBox = Box<int>();
  
  // Upcast to Box<Object>. This is valid because Dart generics are covariant.
  Box<Object> objBox = intBox;

  // Static analysis allows adding a String to Box<Object>.
  // However, the underlying instance is Box<int>.
  // This throws a runtime TypeError similar to the explicit 'covariant' keyword.
  objBox.add("Hello"); 
}
Master Dart with Deep Grasping Methodology!Learn More