Dart Generics

Generics let you avoid specifying concrete types when writing code, instead using type parameters as placeholders, and determining the actual types at the point of use.

The core value of generics isType SafetyandCode Reuse—One piece of code adapts to multiple types, while the compiler checks type errors for you.


Why Generics Are Needed

First, let's look at a pain point without generics.

Example

Writing without generics:

// Without generics: can only store Object, and needs manual casting when retrieving
class IntBox {
  int value;
  IntBox(this.value);
}

class StringBox {
  String value;
  StringBox(this.value);
}

// Every time a new type is added, a new Box class must be written, resulting in a lot of duplicate code

void main() {
  var intBox = IntBox(42);
  var stringBox = StringBox('EXAMPLE');

  print('Integer: ${intBox.value}');
  print('String: ${stringBox.value}');
}

Is there a way to write a Box class that can hold both int and String, while still guaranteeing type safety at compile time?

This is the problem generics aim to solve.


Generic class

A generic class declares type parameters after the class name using angle brackets <>, and uses that type parameter inside the class.

Example

// <T> is a type parameter; T is a placeholder representing "some type"
// Conventional naming: T (Type), E (Element), K (Key), V (Value)
class Box<T> {
  T value;

  Box(this.value);

  T getValue() {
    return value;
  }

  void setValue(T newValue) {
    value = newValue;
  }

  void printValue() {
    print('Value in Box: $value (Type: ${value.runtimeType})');
  }
}

void main() {
  // Specify a concrete type when using a generic class
  var intBox = Box<int>(42);
  var stringBox = Box<String>('EXAMPLE Dart Tutorial');
  var doubleBox = Box<double>(3.14);

  intBox.printValue();
  stringBox.printValue();
  doubleBox.printValue();

  // Type safety: the following code will cause a compile-time error
  // intBox.setValue('hello'); // Error: String cannot be assigned to int

  // Type inference: Dart can infer types from constructor arguments
  var autoBox = Box('Automatically inferred as String');
  print('Inferred type: ${autoBox.value.runtimeType}');
}
Box 中的值: 42 (类型: int)
Box 中的值: EXAMPLE Dart 教程 (类型: String)
Box 中的值: 3.14 (类型: double)
推断类型: String

Multiple Type Parameters

A generic class can have multiple type parameters.

Example

// K represents the type of keys, V represents the type of values
class Pair<K, V> {
  K key;
  V value;

  Pair(this.key, this.value);

  @override
  String toString() => 'Pair($key: $value)';
}

void main() {
  var pair1 = Pair<String, int>('example', 10);
  var pair2 = Pair('score', 95.5);  // Type inference

  print(pair1);
  print(pair2);
}
Pair(example: 10)
Pair(score: 95.5)

Generic methods

In addition to generic classes, functions and methods can also use generics.

Example

// Generic function: return type and parameter type match
// Declare <T> before the return type
T firstElement<T>(List<T> list) {
  if (list.isEmpty) {
    throw ArgumentError('The list cannot be empty');
  }
  return list[0];
}

// Generic function: swap two values
void swap<T>(List<T> list, int i, int j) {
  T temp = list[i];
  list[i] = list[j];
  list[j] = temp;
}

void main() {
  // Dart automatically infers the type when called
  var names = ['example', 'Dart', 'Flutter'];
  print('First name: ${firstElement(names)}');

  var scores = [95, 88, 72];
  print('First score: ${firstElement(scores)}');

  // Swap elements
  var items = ['A', 'B', 'C'];
  print('Before swap: $items');
  swap(items, 0, 2);
  print('After swap: $items');
}
第一个名字: example
第一个分数: 95
交换前: [A, B, C]
交换后: [C, B, A]

Type constraints extends

Sometimes you want a type parameter to be restricted to subtypes of certain types.

Using the extends keyword, you can impose constraints on generic parameters.

Example

// Define a numeric calculator
// <T extends num> constrains T to be num or its subclasses (int, double)
class Calculator<T extends num> {
  T a;
  T b;

  Calculator(this.a, this.b);

  // Because T is constrained to extend num, num's methods can be used safely.
  double add() => (a + b).toDouble();
  double subtract() => (a - b).toDouble();
  double multiply() => (a * b).toDouble();
  double divide() => a / b;

  void printOperations() {
    print('EXAMPLE calculator: $a and $b');
    print(' Add: ${add()}');
    print(' Subtract: ${subtract()}');
    print(' Multiply: ${multiply()}');
    print(' Divide: ${divide().toStringAsFixed(2)}');
  }
}

void main() {
  var intCalc = Calculator<int>(10, 3);
  intCalc.printOperations();

  var doubleCalc = Calculator<double>(3.5, 1.5);
  doubleCalc.printOperations();

  // The following line will report an error: String is not a subclass of num.
  // var strCalc = Calculator<String>('a', 'b'); // Error!
}
EXAMPLE 计算器: 10 和 3
  加: 13.0
  减: 7.0
  乘: 30.0
  除: 3.33
EXAMPLE 计算器: 3.5 和 1.5
  加: 5.0
  减: 2.0
  乘: 5.25
  除: 2.33

Type constraints let you keep the flexibility of generics while also using methods of the constrained type. For example, with the constraint T extends num above, you can use operators such as +, -, *, / in the method body, because the num type supports these operations.


Using Generic Collections

Dart's core collection classes (List, Set, Map) are all generic.

Using generic collections provides compile-time type checking.

Example

void main() {
  // Specify the element type as String
  List<String> names = ['example', 'Dart', 'Flutter'];
  // names.add(42); // Error: int cannot be added to List<String>

  // Specify the key-value types
  Map<String, int> scores = {
    'example': 95,
    'Alice': 87,
  };
  // scores['Bob'] = 'Excellent'; // Error: String cannot be assigned to int

  // Specify the Set element type
  Set<double> prices = {9.99, 19.99, 29.99};

  // Functional operations on generic collections
  List<int> numbers = [1, 2, 3, 4, 5, 6];

  // Type-safe filtering and mapping
  List<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();
  List<String> labels = numbers.map((n) => 'EXAMPLE-$n').toList();

  print(Even numbers: $evenNumbers);
  print(Labels: $labels);

  // Use fold for type-safe accumulation
  int sum = numbers.fold<int>(0, (prev, n) => prev + n);
  print(Sum: $sum);
}
偶数: [2, 4, 6]
标签: [EXAMPLE-1, EXAMPLE-2, EXAMPLE-3, EXAMPLE-4, EXAMPLE-5, EXAMPLE-6]
总和: 21

Practical application: Generic Repository Pattern

One of the most classic applications of generics in real-world development is the "Repository Pattern."

Example

// Define a generic repository interface
abstract class Repository<T> {
  void add(T item);
  void remove(T item);
  T? findById(String id);
  List<T> getAll();
}

// User model
class User {
  String id;
  String name;

  User(this.id, this.name);

  @override
  String toString() => 'User($id, $name)';
}

// Product model
class Product {
  String id;
  String name;
  double price;

  Product(this.id, this.name, this.price);

  @override
  String toString() => 'Product($name, ¥$price)';
}

// Concrete implementation: User repository
class UserRepository implements Repository<User> {
  final List<User> _users = [];

  @override
  void add(User user) => _users.add(user);

  @override
  void remove(User user) => _users.remove(user);

  @override
  User? findById(String id) {
    try {
      return _users.firstWhere((u) => u.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  List<User> getAll() => List.unmodifiable(_users);
}

// Concrete implementation: Product repository (same interface, different types)
class ProductRepository implements Repository<Product> {
  final List<Product> _products = [];

  @override
  void add(Product product) => _products.add(product);

  @override
  void remove(Product product) => _products.remove(product);

  @override
  Product? findById(String id) {
    try {
      return _products.firstWhere((p) => p.id == id);
    } catch (_) {
      return null;
    }
  }

  @override
  List<Product> getAll() => List.unmodifiable(_products);
}

void main() {
  var userRepo = UserRepository();
  userRepo.add(User('u1', 'example'));
  userRepo.add(User('u2', 'Alice'));

  var productRepo = ProductRepository();
  productRepo.add(Product('p1', Dart Tutorial, 29.99));
  productRepo.add(Product('p2', Flutter Tutorial, 49.99));

  print(User list: ${userRepo.getAll()});
  print(Product list: ${productRepo.getAll()});
  print(Find user u1: ${userRepo.findById(u1')}');
  print(Find product p2: ${productRepo.findById(p2')}');
}
用户列表: [User(u1, example), User(u2, Alice)]
商品列表: [Product(Dart 教程, ¥29.99), Product(Flutter 教程, ¥49.99)]
查找用户 u1: User(u1, example)
查找商品 p2: Product(Flutter 教程, ¥49.99)

The generic repository pattern lets you handle different types of data with a unified interface, ensuring type safety while avoiding duplicate code.

other extensions