flutter
/

Dart Generics – Type‑Safe Code with Reusable Components

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Dart Generics – Type‑Safe Code with Reusable Components

What are Generics?

Generics enable you to write code that works with different types while still maintaining type safety. Instead of using dynamic (which disables type checking), generics allow you to specify a type parameter that is resolved at compile time. This leads to reusable components (like collections) that are safe and efficient. Dart's collection classes, such as List<E> and Map<K,V>, are prime examples of generics.

Why Use Generics?

    • Type safety – catch type errors at compile time.
    • Code reuse – write a single class or function that works with many types.
    • Eliminates casting – no need for explicit casts, reducing runtime errors.
    • Better documentation – the expected type is clear from the signature.

Generic Functions

You can declare a generic function by placing a type parameter in angle brackets before the return type. The type parameter can be used in parameters and return types.

DARTRead-only
1
T identity<T>(T value) {
  return value;
}

void main() {
  int x = identity<int>(42);
  String s = identity<String>('Hello');
  // Type inference works too
  var y = identity(3.14); // inferred as double
  print('$x, $s, $y');
}

Generic Classes

A class can have one or more type parameters. You can then instantiate the class with specific types. This is how built‑in collections work.

DARTRead-only
1
class Box<T> {
  T _content;

  Box(this._content);

  T get content => _content;
  void setContent(T value) => _content = value;
}

void main() {
  Box<int> intBox = Box<int>(42);
  print(intBox.content); // 42

  Box<String> stringBox = Box<String>('Hello');
  print(stringBox.content); // Hello
}

Multiple Type Parameters

You can use multiple type parameters, separated by commas. This is common for key‑value containers like maps.

DARTRead-only
1
class Pair<K, V> {
  K key;
  V value;

  Pair(this.key, this.value);

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

void main() {
  var pair = Pair<int, String>(1, 'one');
  print(pair); // 1 → one
}

Type Constraints with extends

Sometimes you need to restrict the types that can be used with a generic. Use the extends keyword to specify an upper bound. The type parameter must be a subtype of the bound.

DARTRead-only
1
class NumberBox<T extends num> {
  T value;
  NumberBox(this.value);

  T add(T other) {
    return (value + other) as T; // OK because T is numeric
  }
}

void main() {
  var intBox = NumberBox<int>(10);
  print(intBox.add(5)); // 15

  var doubleBox = NumberBox<double>(3.14);
  print(doubleBox.add(2.86)); // 6.0

  // var stringBox = NumberBox<String>('test'); // Error: String is not a subtype of num
}

Using Comparable with Bounds

Constraints are often used to ensure that types can be compared or have specific methods. For example, you can require that a type implements Comparable.

DARTRead-only
1
class Max<T extends Comparable<T>> {
  T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
  }
}

void main() {
  var max = Max<int>();
  print(max.max(5, 10)); // 10

  var maxString = Max<String>();
  print(maxString.max('apple', 'banana')); // banana
}

Generic Methods in Non‑Generic Classes

Even if a class isn't generic, you can still have generic methods inside it. The type parameter is scoped to the method.

DARTRead-only
1
class Utility {
  static T identity<T>(T value) => value;

  static T getFirst<T>(List<T> list) => list[0];
}

void main() {
  print(Utility.identity(42));
  print(Utility.getFirst(['a', 'b', 'c'])); // a
}

Understanding dynamic vs Generics

Using dynamic bypasses type checking entirely, which can lead to runtime errors. Generics preserve type information, catching mistakes early.

DARTRead-only
1
void main() {
  List<dynamic> dynamicList = [1, 'two', 3];
  dynamicList.add(4); // allowed, but type unsafe

  List<int> intList = [1, 2, 3];
  // intList.add('four'); // Compile error – type safe
}

Key Takeaways

    • Generics allow writing type‑safe, reusable code.
    • Use <T> for type parameters in classes, methods, and functions.
    • Multiple type parameters are supported (e.g., <K, V>).
    • Use extends to constrain type parameters to a supertype.
    • Generics help avoid casting and reduce runtime errors.
    • Prefer generics over dynamic when possible.

Try it yourself

class Box<T> {
  T value;
  Box(this.value);
  void display() => print('Box contains: $value');
}

void main() {
  Box<int> intBox = Box(42);
  intBox.display();

  Box<String> strBox = Box('Hello');
  strBox.display();
}

Test Your Knowledge

Q1
of 4

What is the purpose of generics in Dart?

A
To write code that works with any type without type safety
B
To write reusable, type‑safe code
C
To make code run faster
D
To replace classes
Q2
of 4

How do you declare a generic function with a type parameter T?

A
T function<T>(T value) {}
B
<T> T function(T value) {}
C
function<T>(T value) {}
D
T function(T value) {}
Q3
of 4

What does `class Box<T extends num>` mean?

A
Box can only contain num values
B
T must be a subtype of num
C
T must be exactly num
D
T can be any type
Q4
of 4

Which of the following is a valid generic constraint?

A
T extends Comparable
B
T implements Comparable
C
T is Comparable
D
T as Comparable

Frequently Asked Questions

What is the difference between `List` and `List<dynamic>`?

List without a type parameter is shorthand for List<dynamic>, meaning it can hold any type, but you lose type safety. List<dynamic> allows mixing types; List<int> restricts to integers and gives compile‑time type checking.

Can I use `extends` with multiple bounds?

Yes, you can use extends to require a class to extend a superclass and implement interfaces. For example: class C<T extends A & B>. The bound can combine a superclass (at most one) and any number of interfaces using &.

How does Dart handle generic type erasure?

Dart does not have type erasure like Java; type parameters are reified, meaning the type is available at runtime. You can check types with is (e.g., list is List<int>).

Can I instantiate a generic type?

Not directly. For example, you cannot write T() if T is a type parameter. You need to pass a factory or a constructor function to create instances.

What are the naming conventions for type parameters?

Common conventions: E for element type (in collections), K and V for key and value, T for a single type, S and U for additional types. Use descriptive names when appropriate (e.g., KeyType, ValueType).

Previous

dart custom exceptions

Next

dart extension methods

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.