felangel / felangel/equatable

toString() in Equatable and EquatableMixin silently overrides superclass implementations

Open
#212 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Dart
Stars
1.1k
Forks
118
PR merge metrics
No merged PRs in 30d

Description

# Pull Request
https://github.com/felangel/equatable/pull/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:

```dart
@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

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

This change applies to both:
- `equatable.dart`
- `equatable_mixin.dart`

---

## Behavior Comparison (`EquatableMixin`)

### Before

```dart
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 get props => [name];
}

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

### After

```dart
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 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.