Skip to main content
A generic class in Dart is a class parameterized over types. It utilizes type variables (conventionally denoted by single uppercase letters such as T, E, K, or V) as placeholders for specific data types. This mechanism allows the class structure to enforce strict compile-time type safety across its fields, methods, and constructors without committing to a concrete type during declaration.

Syntax and Declaration

To declare a generic class, append the type parameter enclosed in angle brackets (< >) immediately following the class name. The type parameter is then available in the class’s lexical scope.
class Wrapper<T> {
  T data;

  Wrapper(this.data);

  T retrieveData() {
    return data;
  }
}
A class can declare multiple type parameters by separating them with commas.
class Pair<K, V> {
  final K key;
  final V value;

  Pair(this.key, this.value);
}

Type Bounds (Constraints)

By default, a type parameter T is equivalent to T extends Object?, meaning it accepts any nullable or non-nullable type. To restrict the types that can be passed as a type argument, Dart uses the extends keyword to define a type bound. If a type argument does not match the bound or a subtype of the bound, the Dart analyzer throws a compile-time error.
class NumericProcessor<T extends num> {
  final T value;

  NumericProcessor(this.value);

  double getDouble() => value.toDouble();
}

// Valid: int is a subtype of num
var intProcessor = NumericProcessor<int>(42);

// Invalid: String is not a subtype of num (Compile-time error)
// var stringProcessor = NumericProcessor<String>('42');

Instantiation and Type Inference

When instantiating a generic class, the type argument can be explicitly declared. However, Dart’s type inference engine can automatically resolve the type argument based on the constructor’s arguments.
// Explicit type declaration
var explicitWrapper = Wrapper<String>('Dart');

// Inferred type declaration (Inferred as Wrapper<double>)
var inferredWrapper = Wrapper(3.14); 

Reified Generics

Unlike languages that use type erasure (such as Java), Dart implements reified generics. This means that generic classes retain their type arguments at runtime. You can perform runtime type checks against the specific parameterized type.
var stringWrapper = Wrapper<String>('Hello');

// Evaluates to true at runtime
print(stringWrapper is Wrapper<String>); 

// Evaluates to false at runtime
print(stringWrapper is Wrapper<int>); 

// Accessing the runtime type directly
print(stringWrapper.runtimeType); // Outputs: Wrapper<String>
Tired of Poor Dart Skills? Fix That With Deep Grasping!Learn More