SkillByAIOpen interactive version →

Lesson 15 / 25

Generics, Extension Methods and Extension Types

Write generic code and extend existing types safely.

Reusable and extensible code

Generics let classes and functions work with any type while keeping type safety: class Cache<K, V>, T firstOr<T>(List<T> items, T fallback). Bound type parameters restrict them: T extends Comparable<T>. Dart generics are reified, so the runtime knows List<int> from List<String> and is List<int> checks work. Extension methods add methods, getters and operators to existing types you do not control, such as adding toRupees() to int or capitalised to String; they are resolved statically by the variable's type and must be imported to be visible. Extension types (Dart 3.3) wrap an existing type in a new static type with zero run-time cost: extension type UserId(String value) {} prevents accidentally passing an email string where a user ID is expected, while compiling down to the underlying String. They are useful for IDs, units and safer interop wrappers, but because the wrapper disappears at run time, they are not a security boundary: a cast can bypass them.

Generics, an extension and an extension type

Type-safe reuse and zero-cost wrappers.

class Cache<K, V> {
  final _store = <K, (V, DateTime)>{};
  final Duration ttl;
  Cache(this.ttl);

  V? get(K key) {
    final entry = _store[key];
    if (entry == null) return null;
    final (value, storedAt) = entry;
    return DateTime.now().difference(storedAt) < ttl ? value : null;
  }

  void put(K key, V value) => _store[key] = (value, DateTime.now());
}

T maxOf<T extends Comparable<T>>(List<T> items) =>
    items.reduce((a, b) => a.compareTo(b) >= 0 ? a : b);

extension RupeeFormat on int {
  String get asRupees => 'Rs ${(this / 100).toStringAsFixed(2)}';   // int paise -> text
}

extension type OrderId(String value) {
  OrderId.parse(String raw) : value = raw.trim().toUpperCase();
  bool get isValid => value.startsWith('ORD-');
}

void cancel(OrderId id) => print('Cancelling ${id.value}');

void main() {
  final prices = Cache<String, int>(const Duration(minutes: 5))..put('pen', 4950);
  print(prices.get('pen')?.asRupees);   // Rs 49.50
  print(maxOf([3, 9, 4]));               // 9
  cancel(OrderId.parse(' ord-42 '));
  // cancel('ORD-42');                   // compile error: String is not an OrderId
}

Extensions must be imported

Extension methods are only available where their library is imported. If a method seems to vanish in another file, check the import before suspecting the compiler.

Quick check: What is the main benefit of an extension type such as OrderId(String value)?

  • It adds run-time validation automatically
  • It makes the value mutable
  • It gives a distinct static type with no run-time overhead, preventing mix-ups with plain strings
  • It replaces generics
Answer

It gives a distinct static type with no run-time overhead, preventing mix-ups with plain strings — Extension types are compile-time wrappers that erase to the underlying type at run time.