Skip to content

Unable to use draft with equatable #7

Closed

Description

@tomwyr

Consider the following example:

part 'user.draft.dart';

@draft
class User extends Equatable {
  User({required this.name});

  final int name;

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

The generated draft class implements User, which adds an implicit conformance requirement to Equatable.
Given how the generator currently works, the generated output won’t compile due to a missing member required by Equatable.

// ❌ Missing concrete implementation of 'getter abstract class Equatable.stringify'. Try implementing the missing method, or make the class abstract.
class UserDraft implements User {
  // Mutable fields
  int name;

  // Getters and setters for nested draftable fields

  UserDraft({required this.name});

  User save() => User(name: name);

  @override
  List<Object?> get props => save().props;
}

Given how ubiquitous packages like equatable are, it would be convenient if draft allowed marking classes that declare explicit supertypes as draftable.

Some obvious solutions that come to mind are:

  • Forward getters and methods from supertypes in a similar fashion to forwarding members of the annotated type.
  • Don't implementing the annotated type in the generated draft class.
    • The motivation here is that the responsibility of the draft type is to provide a way to update fields of its immutable counterpart and implementing the annotated type may go beyond that responsibility while increasing the complexity of the generated code.

There might be some additional context I’m not aware of, such as how immerjs works or draft’s prior design decisions, so I’m not entirely sure what the preferable approach would be.

Activity

  1. josiahsrc commented on Dec 30, 2025

    @josiahsrc
    Owner

    @tomwyr Another option I've used here is the EquatableMixin mixin. It will extend the class without requiring a concrete implementation. Does this work for you?

    part 'user.draft.dart';
    
    @draft
    class User with EquatableMixin { // <-- Change here
      User({required this.name});
    
      final int name;
    
      @override
      List<Object?> get props => [name];
    }
  2. tomwyr commented on Dec 30, 2025

    @tomwyr
    CollaboratorAuthor

    Changing the declaration to use the mixin instead does help, thanks.

    Originally, I encountered this issue while trying to annotate a subclass of a sealed type as draftable, roughly as follows:

    sealed class State extends Equatable {
      @override
      List<Object?> get props => [];
    }
    
    class Loading extends State {}
    
    @draft
    class Data extends State {
      final String value;
    
      Data({required this.value});
    
      @override
      List<Object?> get props => [value];
    }
  3. josiahsrc commented on Dec 30, 2025

    @josiahsrc
    Owner

    Ah, gotcha. I suppose if we wanted to support that, we could make the generated draft class implement the super interfaces also, and then call the main class's methods by saving first then invoking them. From your example:

    class DataDraft implements Data {
       @override
      List<Object?> get props => save().props; // convert back to Data, then ret props
    }
  4. tomwyr commented on Dec 30, 2025

    @tomwyr
    CollaboratorAuthor

    I believe that would be sufficient to support extending base classes.

    As far as I can see, in the generator we could leverage classElement.allSupertypes or
    classElement.inheritedConcreteMembers/classElement.inheritedMembers to access members of the base type(s) included in the generated code too. There might be some additional work required to deduplicate fields (what if the draftable class overrides a getter or a method).

  5. josiahsrc commented on Dec 31, 2025

    @josiahsrc
    Owner

    Sounds like a plan

  6. tomwyr commented on Jan 1, 2026

    @tomwyr
    CollaboratorAuthor

    I might give it a try in the coming days and draft a PR to validate the idea, verify there will be no caveats, etc.

  7. josiahsrc commented on Jan 1, 2026

    @josiahsrc
    Owner

    Sweet, I'll keep an eye out. Happy new year @tomwyr !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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