Skip to main content
A generic constraint restricts the allowable types for a type parameter by defining a mandatory supertype, referred to as the upper bound. This mechanism ensures that any type argument provided at compile time is a subtype of the specified bound, enabling type-safe access to the bound’s interface within the generic implementation.

Syntax

Constraints are declared using the extends keyword within the angle brackets < > of a generic declaration. Constraints can be applied to classes, mixins, enums, extensions, extension types, functions, and typedefs.
// T is the type parameter
// SomeClass is the upper bound
class DataContainer<T extends SomeClass> {
  final T data;
  DataContainer(this.data);
}

Type Safety and Member Access

By establishing a constraint, the Dart compiler guarantees that the type parameter T exposes the interface of the upper bound. This permits the implementation to access methods and properties defined in the bound type on instances of T without requiring explicit casts.
abstract class Shape {
  double get area;
}

// The constraint allows access to .area on the variable 'shape'
class AreaCalculator<T extends Shape> {
  double calculate(T shape) {
    return shape.area; // Valid because T is guaranteed to be a Shape
  }
}

Compile-Time Validation

The Dart analyzer enforces constraints statically. Attempting to instantiate a generic type or invoke a generic function with a type argument that does not satisfy the bound results in a compile-time error.
class Box<T extends num> {}

void main() {
  var intBox = Box<int>();       // Valid: int is a subtype of num
  var doubleBox = Box<double>(); // Valid: double is a subtype of num
  
  // Compile-time Error: String is not a subtype of num
  // var stringBox = Box<String>(); 
}

Recursive Generic Constraints

Dart supports F-bounded polymorphism, allowing a type parameter to be constrained by a generic type that is parameterized by the type parameter itself. This is required when a type must interact with other instances of the same specific type, such as in comparison operations.
// T must implement Comparable specifically for type T
class Sorter<T extends Comparable<T>> {
  int compare(T a, T b) {
    return a.compareTo(b);
  }
}

Nullability and Default Bounds

If no constraint is specified, the default bound is Object?, which permits nullable types. To enforce non-nullability, the type parameter must be explicitly constrained to Object or a non-nullable subtype.
  • Nullable (Implicit): <T> is equivalent to <T extends Object?>. It accepts String, String?, and dynamic.
  • Non-nullable: <T extends Object> ensures T cannot be a nullable type (e.g., int?). This also excludes dynamic, as dynamic inherently permits null.
Master Dart with Deep Grasping Methodology!Learn More