Dart: Dart Generics — Type-Safe and Reusable Programming
Last updated: 2026-08-26
Generics are templates for code — write once, reuse for many types, and the compiler ensures type safety for you.
1. What You Will Learn
- Defining generic classes and generic functions
- Generic constraints:
extendsand multiple bounds - Collaboration between generics and collections
- Covariance and contravariance (type bounds)
- Bob's scenario: Generic data transformer
DataTransformer<T, R>in DataPipeline
2. A Developer's Real Story
(1) The Pain: Maintenance Nightmare from Duplicate Code
Charlie wrote separate parsers for each data type in DataPipeline: OrderParser, ProductParser, CustomerParser. The logic in all three parsers was nearly identical (read → validate → transform), only differing in input/output types. Every time the parsing logic needed modification, it had to be changed in 3 places. Once, a missed change meant the Product parser didn't apply the new validation rules.
(2) The Generic Solution
Unify the three parsers using a generic class DataTransformer<T, R>. The type parameters let the compiler ensure type safety, and the code only needs to be maintained in one place.
abstract class DataTransformer<T, R> {
R transform(T input);
List``<R>`` batch(List``<T>`` inputs) => inputs.map(transform).toList();
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
(3) The Benefits
- 3 parsers merged into 1 generic class, reducing code volume by 70%
- Modifying validation logic requires changing only one place; all types update automatically
- Compiler ensures type safety, preventing mixing up Orders with Products
3. Generics Basics
(1) Why Generics Are Needed
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Problems without generics
// Without generics - List``<dynamic>`` loses type safety
void main() {
List amounts = [1500, 3200, 890]; // List``<dynamic>``
amounts.add('not a number'); // No compile error!
for (final a in amounts) {
print((a as int) * 2); // Runtime crash on 'not a number'
}
}
// With generics - compile-time type safety
void main() {
List``<int>`` amounts = [1500, 3200, 890];
// amounts.add('not a number'); // Compile error!
for (final a in amounts) {
print(a * 2); // Safe - type is known
}
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
4. Generic Classes
(1) Generic Class Definition
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Generic container class
class Result``<T>`` {
final T? data;
final String? error;
final bool isSuccess;
Result.success(this.data)
: error = null,
isSuccess = true;
Result.failure(this.error)
: data = null,
isSuccess = false;
// Method that returns the generic type
T get dataOrThrow {
if (isSuccess && data != null) return data;
throw Exception(error ?? 'Unknown error');
}
// Transform the success value
Result``<R>`` map``<R>``(R Function(T) fn) {
if (isSuccess && data != null) {
return Result.success(fn(data));
}
return Result.failure(error);
}
}
void main() {
final success = Result.success(1500.0);
final failure = Result``<double>``.failure('Network timeout');
print(success.data); // 1500.0
print(failure.error); // Network timeout
// Map transforms the success value
final formatted = success.map((v) => '\$${v.toStringAsFixed(2)} USD');
print(formatted.data); // $1500.00 USD
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Generic cache class
class Cache``<T>`` {
final Map<String, T> _store = {};
final Duration _ttl;
Cache({Duration ttl = const Duration(minutes: 5)}) : _ttl = ttl;
void put(String key, T value) => _store[key] = value;
T? get(String key) => _store[key];
bool contains(String key) => _store.containsKey(key);
void remove(String key) => _store.remove(key);
void clear() => _store.clear();
}
void main() {
final orderCache = Cache<Map<String, dynamic>>();
orderCache.put('ORD-001', {'amount': 1500.0, 'status': 'completed'});
print(orderCache.get('ORD-001')); // {amount: 1500.0, status: completed}
print(orderCache.contains('ORD-002')); // false
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
5. Generic Functions
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Generic function definition
// Generic function with single type parameter
T firstOrNull``<T>``(List``<T>`` items) => items.isEmpty ? throw StateError('Empty list') : items.first;
// Generic function with two type parameters
R transform<T, R>(T input, R Function(T) converter) => converter(input);
// Generic function with constraint
double sumNumbers<T extends num>(List``<T>`` items) =>
items.fold(0.0, (sum, item) => sum + item.toDouble());
void main() {
print(firstOrNull(['ORD-001', 'ORD-002'])); // ORD-001
print(firstOrNull([1500, 3200])); // 1500
final formatted = transform(1500.0, (v) => '\$${v} USD');
print(formatted); // $1500.0 USD
print(sumNumbers([1, 2, 3, 4, 5])); // 15.0
print(sumNumbers([1.5, 2.5, 3.0])); // 7.0
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Generic utility functions
// Safe cast utility
T? tryCast``<T>``(dynamic value) {
if (value is T) return value;
return null;
}
// Partition a list into two groups
(List``<T>``, List``<T>``) partition``<T>``(List``<T>`` items, bool Function(T) predicate) {
final matching = ``<T>``[];
final notMatching = ``<T>``[];
for (final item in items) {
(predicate(item) ? matching : notMatching).add(item);
}
return (matching, notMatching);
}
void main() {
// Safe cast
final intVal = tryCast``<int>``('hello'); // null
final strVal = tryCast``<String>``('hello'); // hello
// Partition
final (high, low) = partition([1500.0, 50.0, 3200.0, 890.0], (v) => v >= 1000);
print('High: $high'); // [1500.0, 3200.0]
print('Low: $low'); // [50.0, 890.0]
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
6. Generic Constraints
(1) Single extends Constraint
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: extends constraint
// Only accepts types that extend num
double average<T extends num>(List``<T>`` values) {
if (values.isEmpty) return 0;
final sum = values.fold``<num>``(0, (a, b) => a + b);
return sum / values.length;
}
// Only accepts types that implement Comparable
T findMax<T extends Comparable>(List``<T>`` items) {
if (items.isEmpty) throw StateError('Empty list');
return items.reduce((a, b) => a.compareTo(b) >= 0 ? a : b);
}
void main() {
print(average([10, 20, 30])); // 20.0
print(average([1.5, 2.5, 3.0])); // 2.333...
print(findMax(['banana', 'apple', 'cherry'])); // cherry
print(findMax([10, 5, 8])); // 10
// average(['a', 'b']); // Compile error! String doesn't extend num
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
(2) Multiple Constraints
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Multiple constraints
// Multiple constraints using intersection
class SortedCollection<T extends Comparable``<T>``> {
final List``<T>`` _items = [];
void add(T item) {
final index = _items.indexWhere((e) => item.compareTo(e) <= 0);
if (index == -1) {
_items.add(item);
} else {
_items.insert(index, item);
}
}
List``<T>`` get items => List.unmodifiable(_items);
}
void main() {
final sorted = SortedCollection``<String>``();
sorted.add('cherry');
sorted.add('apple');
sorted.add('banana');
print(sorted.items); // [apple, banana, cherry]
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
7. Collaboration between Generics and Collections
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: Type-safe collection operations
// Type-safe key-value store
class KeyValueStore<K, V> {
final Map<K, V> _data = {};
void put(K key, V value) => _data[key] = value;
V? get(K key) => _data[key];
// Convert all values
Map<K, R> mapValues``<R>``(R Function(V) converter) {
return _data.map((key, value) => MapEntry(key, converter(value)));
}
// Filter by key type
Map<K, V> whereKey(bool Function(K) predicate) {
return Map.fromEntries(
_data.entries.where((e) => predicate(e.key)),
);
}
}
void main() {
final store = KeyValueStore<String, double>();
store.put('Electronics', 3600.0);
store.put('Books', 170.0);
store.put('Clothing', 890.0);
// Map values to formatted strings
final formatted = store.mapValues((v) => '\$${v.toStringAsFixed(2)} USD');
print(formatted); // {Electronics: $3600.00 USD, Books: $170.00 USD, Clothing: $890.00 USD}
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
8. Bob's Scenario: Generic Data Transformer
classDiagram
class DataTransformer~T, R~ {
+transform(T input) R
+batch(List~T~ inputs) List~R~
+validate(T input) bool
}
class OrderParser {
+transform(String) Order
}
DataTransformer <|-- OrderParser
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
▶ Example
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
: DataTransformer generic framework
// Base transformer interface
abstract class DataTransformer<T, R> {
R transform(T input);
String get name;
List``<R>`` batch(List``<T>`` inputs) {
final results = ``<R>``[];
final errors = <(int, String)>[];
for (var i = 0; i < inputs.length; i++) {
try {
if (validate(inputs[i])) {
results.add(transform(inputs[i]));
}
} catch (e) {
errors.add((i, e.toString()));
}
}
if (errors.isNotEmpty) {
print('$name: ${errors.length} errors during batch transform');
}
return results;
}
bool validate(T input) => true;
}
// Concrete transformer: String CSV line → Order
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)})';
}
class CsvOrderTransformer extends DataTransformer<String, Order> {
@override
String get name => 'CsvOrderTransformer';
@override
bool validate(String input) {
final parts = input.split(',');
return parts.length >= 2;
}
@override
Order transform(String input) {
final parts = input.split(',');
return Order(
id: parts[0].trim(),
amount: double.parse(parts[1].trim()),
);
}
}
void main() {
final transformer = CsvOrderTransformer();
final csvLines = [
'ORD-001, 1500.0',
'ORD-002, 3200.0',
'ORD-003, 890.0',
];
final orders = transformer.batch(csvLines);
print('Transformed ${orders.length} orders:');
for (final order in orders) {
print(' $order');
}
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
9. Complete Example: DataPipeline Generic Processing Framework
// ============================================
// DataPipeline Generic Processing Framework
// Type-safe data transformation pipeline
// ============================================
// Result type for safe error handling
class Result``<T>`` {
final T? data;
final String? error;
Result.success(this.data) : error = null;
Result.failure(this.error) : data = null;
bool get isSuccess => data != null;
Result``<R>`` map``<R>``(R Function(T) fn) {
if (isSuccess && data != null) return Result.success(fn(data!));
return Result.failure(error);
}
}
// Generic transformer
abstract class DataTransformer<T, R> {
R transform(T input);
String get name;
bool validate(T input) => true;
Result``<R>`` safeTransform(T input) {
try {
if (!validate(input)) {
return Result.failure('$name: Validation failed');
}
return Result.success(transform(input));
} catch (e) {
return Result.failure('$name: $e');
}
}
List<Result``<R>``> batch(List``<T>`` inputs) =>
inputs.map(safeTransform).toList();
}
// Generic aggregator
abstract class DataAggregator<T, R> {
R aggregate(List``<T>`` items);
String get name;
}
// Sum aggregator for numeric types
class SumAggregator<T extends num> extends DataAggregator<T, double> {
@override
String get name => 'SumAggregator';
@override
double aggregate(List``<T>`` items) =>
items.fold(0.0, (sum, item) => sum + item.toDouble());
}
// Average aggregator
class AverageAggregator<T extends num> extends DataAggregator<T, double> {
@override
String get name => 'AverageAggregator';
@override
double aggregate(List``<T>`` items) =>
items.isEmpty ? 0 : items.fold(0.0, (s, i) => s + i.toDouble()) / items.length;
}
// Pipeline that chains transformers
class Pipeline<I, M, O> {
final DataTransformer<I, M> _first;
final DataTransformer<M, O> _second;
Pipeline(this._first, this._second);
List<Result``<O>``> process(List``<I>`` inputs) {
final midResults = _first.batch(inputs);
final midValues = midResults.where((r) => r.isSuccess).map((r) => r.data!).toList();
return _second.batch(midValues);
}
}
// Concrete types
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)})';
}
class OrderSummary {
final String id;
final String tier;
OrderSummary({required this.id, required this.tier});
@override
String toString() => 'OrderSummary($id, $tier)';
}
class CsvToOrder extends DataTransformer<String, Order> {
@override
String get name => 'CsvToOrder';
@override
Order transform(String input) {
final parts = input.split(',');
return Order(id: parts[0].trim(), amount: double.parse(parts[1].trim()));
}
@override
bool validate(String input) => input.split(',').length >= 2;
}
class OrderToSummary extends DataTransformer<Order, OrderSummary> {
@override
String get name => 'OrderToSummary';
@override
OrderSummary transform(Order input) {
final tier = input.amount >= 1000 ? 'Premium' : 'Standard';
return OrderSummary(id: input.id, tier: tier);
}
}
void main() {
final csvLines = [
'ORD-001, 1500.0',
'ORD-002, 50.0',
'ORD-003, 3200.0',
];
// Simple transformer
final parser = CsvToOrder();
final orders = parser.batch(csvLines);
print('=== Parsed Orders ===');
for (final result in orders) {
if (result.isSuccess) {
print(' ${result.data}');
} else {
print(' Error: ${result.error}');
}
}
// Pipeline: CSV → Order → Summary
final pipeline = Pipeline<String, Order, OrderSummary>(
CsvToOrder(), OrderToSummary());
final summaries = pipeline.process(csvLines);
print('\n=== Order Summaries ===');
for (final result in summaries) {
if (result.isSuccess) print(' ${result.data}');
}
// Aggregation
final validAmounts = orders
.where((r) => r.isSuccess)
.map((r) => r.data!.amount)
.toList();
final total = SumAggregator().aggregate(validAmounts);
final avg = AverageAggregator().aggregate(validAmounts);
print('\nRevenue: \$${total.toStringAsFixed(2)} USD');
print('Average: \$${avg.toStringAsFixed(2)} USD');
}
> **Output:** Run in local DartPad or with `dart run`. All Dart examples are based on Dart 3.x / Flutter 3.x, results may vary slightly with SDK version.
Output:
=== Parsed Orders ===
Order(ORD-001, $1500.00)
Order(ORD-002, $50.00)
Order(ORD-003, $3200.00)
=== Order Summaries ===
OrderSummary(ORD-001, Premium)
OrderSummary(ORD-002, Standard)
OrderSummary(ORD-003, Premium)
Revenue: $4750.00 USD
Average: $1583.33 USD
❓ FAQ
Q: Do generic type parameters exist at runtime? A: Yes, Dart's generics are reified. You can get type information at runtime. `list is List``
``` returns true at runtime, unlike Java's type erasure.
Q: Can a generic constraint have multiple bounds? A: Yes, you can use
T extends A & Bsyntax (though Dart doesn't currently support&for multiple bounds directly). You can achieve it indirectly by having the constrained type implement multiple interfaces, or use bounds like `T extends Comparable`````.
Q: When should I use generics vs. dynamic? A: Use generics whenever possible. Generics check types at compile-time, while
dynamicreveals errors only at runtime. Usedynamiconly when the type is truly unknown (like JSON parsing).
Q: Can a generic method have different type parameters than its class? A: Yes. For example,
<T, R> R transform(T input, R Function(T) fn)has two type parameters. A method's type parameters are independent of its class's type parameters.
**Q: What's the difference between
List``<dynamic>`` andList<T>```?** A: `Listaccepts elements of any type, losing type safety. `List``<T>only accepts elements of type T, with compiler-guaranteed type safety.List``<dynamic>``` is not a supertype of otherList````` types.
Q: Is there a difference between
extendsandimplementsin generic constraints? A: In constraints,extendsmatches both class inheritance and interface implementation.T extends Comparablemeans T can be a subclass or an implementing class of Comparable.
Q: What is the
covariantkeyword used for? A:covariantallows a subtype to use a more specific type for a parameter (covariance). It's often used for parameters in consumer patterns but weakens type safety—use it cautiously.
📖 Summary
- Generics allow code to be reused for multiple types via type parameters
<T>, with the compiler ensuring type safety. - Generic classes (
class Box<T>) and generic functions (T first<T>(List<T>)) are the most common forms. - The
extendsconstraint limits the range of type parameters, e.g.,<T extends num>accepts only numeric types. - Generic collections (
List<T>,Map<K, V>) are safer thanList<dynamic>. - DataPipeline's
DataTransformer<T, R>is a typical application of generic design.
📝 Exercises
- Basic (Difficulty ⭐): Implement a generic
Pair<T, U>class with fieldsfirstandsecond, and aswap()method that returns a new Pair with swapped values. Test withPair<int, String>andPair<double, bool>. - Intermediate (Difficulty ⭐⭐): Implement a
Result<T>type (similar to Rust's Result) withisSuccess,data,error, and amap<R>()method. Use it to rewrite a parsing function that can fail. - Challenge (Difficulty ⭐⭐⭐): Implement a generic
Pipeline<T, R>class that supports chaining multipleDataTransformers, automatically infers intermediate types, and finally callsprocess()to execute the entire pipeline. Hint: You can use recursive types or the builder pattern.