Skip to content

toString() in Equatable and EquatableMixin silently overrides superclass implementations #212

@stan-at-work

Description

@stan-at-work

Pull Request

#213

Summary

Change the default toString() implementation in equatable.dart and equatable_mixin.dart to delegate to super.toString() instead of returning '$runtimeType' directly. For EquatableMixin, this allows classes that mix it in to correctly inherit toString() behavior from their superclass hierarchy. For Equatable, this is a consistency alignment with standard Dart behavior.


Motivation

Currently, the default toString() implementation in both Equatable and EquatableMixin is:

@override
String toString() => '$runtimeType';

EquatableMixin

Because EquatableMixin is a mixin, it can be applied to a class that already extends another class with a meaningful toString(). In this scenario, the mixin silently shadows that superclass implementation with a bare runtime type string without any visible indication in the consuming code.

This is a source of subtle, hard-to-detect bugs. The behavior changes as soon as EquatableMixin is added to the class declaration, and there is no way for the developer to notice unless they explicitly verify the output. Changing the implementation to return super.toString() restores the expected Dart method resolution order (MRO), allowing superclass toString() implementations to propagate correctly through the inheritance chain.

Equatable

Since Equatable is an abstract class that does not extend any class other than Object, changing to super.toString() does not unlock any superclass inheritance benefit. The call simply resolves to Object.toString(), which returns Instance of 'ClassName' instead of the current '$runtimeType'. The motivation here is purely consistency aligning the behavior with standard Dart conventions rather than introducing a custom format.


Proposed Change

- @override
- String toString() => '$runtimeType';
+ @override
+ String toString() => super.toString();

This change applies to both:

  • equatable.dart
  • equatable_mixin.dart

Behavior Comparison (EquatableMixin)

Before

import 'package:equatable/equatable.dart';

abstract class Animal {
  const Animal();

  @override
  String toString() => 'THIS IS A ANIMAL';
}

class Dog extends Animal with EquatableMixin {
  const Dog({required this.name});

  final String name;

  @override
  List<Object?> get props => [name];
}

void main() {
  print(const Dog(name: 'Spot')); // 'Dog'  ← superclass toString() is silently ignored
}

After

import 'package:equatable/equatable.dart';

abstract class Animal {
  const Animal();

  @override
  String toString() => 'THIS IS A ANIMAL';
}

class Dog extends Animal with EquatableMixin {
  const Dog({required this.name});

  final String name;

  @override
  List<Object?> get props => [name];
}

void main() {
  print(const Dog(name: 'Spot')); // 'THIS IS A ANIMAL'  ← superclass toString() is now respected
}

Note that Dog itself is unchanged the output changes solely because EquatableMixin no longer silently swallows the superclass implementation.


Impact on Existing Codebases

This is a behavioral breaking change for any class that:

  1. Mixes in EquatableMixin, and
  2. Does not override toString() itself, and
  3. Inherits a non-default toString() from a superclass.

Additionally, any class extending Equatable or mixing in EquatableMixin that relies on the current '$runtimeType' string format will observe a change to Instance of 'ClassName'. Teams are advised to audit usages of .toString() (including implicit calls via string interpolation or print) on classes that extend or mix in equatable abstractions.


Notes

  • Classes that already define their own toString() override are most likely unaffected.
  • The superclass propagation benefit described above applies exclusively to EquatableMixin, as it is the only one that can be combined with an independent class hierarchy.
  • This change aligns equatable with the principle of least surprise and the expected Dart method resolution order.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions