Skip to content

Self type leads to typing problems #60088

Closed
@julien4215

Description

@julien4215

When trying to define a self type like this class Foo<Self extends Foo<Self>> it is unclear if the methods of the class that return instances of itself should use Self or Foo<Self> as the return type.

  • Using Self as the return type might cause issues when you need to use the type Foo. This will happen when you know only at runtime which subclass of Foo is used.
abstract class Foo<Self extends Foo<Self>> {
  final bool property;

  const Foo(this.property);

  Self copyWith(bool property);

  Self toggle() {
    return copyWith(!property);
  }
}

class Bar extends Foo<Bar> {
  const Bar(super.property);

  @override
  Bar copyWith(bool property) {
    return Bar(property);
  }
}

void main() {
  // Let's pretend that there are multiple subclass of `Foo`
  // and that you know only at runtime which one you will get 
  // so that you need to use the type `Foo`
  Foo someFoo = const Bar(true); // type: Foo<Foo<dynamic>>

  someFoo = someFoo.toggle(); // type: Foo<dynamic>
}

The static analysis fails because when applying the toggle method the type becomes Foo<dynamic> instead of Foo<Foo<dynamic>>.

  • Using Foo<Self> as the return type doesn't mix well with using the name of the subclass as the type. In this example, it would preferable to be able to use the type Bar for the variable someFoo but this is not possible as the toggle method has a return type Foo<Bar>.
abstract class Foo<Self extends Foo<Self>> {
  final bool property;

  const Foo(this.property);

  Foo<Self> copyWith(bool property);

  Foo<Self> toggle() {
    return copyWith(!property);
  }
}

class Bar extends Foo<Bar> {
  const Bar(super.property);

  @override
  Bar copyWith(bool property) {
    return Bar(property);
  }
}

void main() {
  Bar someFoo = const Bar(true); // type: Bar

  someFoo = someFoo.toggle(); // type: Foo<Bar>
}

The static analysis fails because someFoo.toggle() uses the type Foo<Bar> and the variable someFoo is defined with type Bar.


Related to dart-lang/language#3025

Activity

dart-github-bot

dart-github-bot commented on Feb 10, 2025

@dart-github-bot
Collaborator

Summary: Self-referential type parameters in Dart cause type inference issues. Using Self or Foo<Self> as return types leads to static analysis failures when dealing with runtime subclass selection.

added
triage-automationSee https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot.
type-bugIncorrect behavior (everything from a crash to more subtle misbehavior)
on Feb 10, 2025
eernstg

eernstg commented on Feb 10, 2025

@eernstg
Member

This is all working as specified and intended.

abstract class Foo<Self extends Foo<Self>> {
  final bool property;

  const Foo(this.property);

  Self copyWith(bool property);

  Self toggle() {
    return copyWith(!property);
  }
}

class Bar extends Foo<Bar> {
  const Bar(super.property);

  @override
  Bar copyWith(bool property) {
    return Bar(property);
  }
}

class Baz extends Foo<Baz> {
  const Baz(super.property);

  @override
  Baz copyWith(bool property) {
    return Baz(property);
  }
}

var b = true; // Random.

void main() {
  // Note that the type annotation `Foo` means `Foo<Foo<dynamic>>`, no
  // matter how it's initialized. So let's write that out explicitly.
  // With that, you can certainly have multiple subclasses of `Foo`
  // and abstract over them using a type like `Foo<Foo<dynamic>>`.
  Foo<Foo<dynamic>> someFoo = b ? const Bar(true) : Baz(false);

  // But you can't assign a `Foo<dynamic>` to a variable of type
  // `Foo<Foo<dynamic>>`, because `dynamic` must then be a subtype
  // of `Foo<dynamic>` (which it isn't). So this is an error:
  someFoo = someFoo.toggle();
}

Here is the other example, adjusted similarly:

abstract class Foo<Self extends Foo<Self>> {
  final bool property;

  const Foo(this.property);

  Self copyWith(bool property);

  Self toggle() {
    return copyWith(!property);
  }
}

class Bar extends Foo<Bar> {
  const Bar(super.property);

  @override
  Bar copyWith(bool property) {
    return Bar(property);
  }
}

void main() {
  Bar someBar = const Bar(true);
  someBar = someBar.toggle();
}

In this case (and in general, actually) you should change the return type of the methods from Foo<Self> to Self. Note that Self is a subtype of Foo<Self> (which is guaranteed because we know that, by soundness, the type variable bounds are enforced at run time for every object). In short Self is a better Self type than Foo<Self>.

This means that it's just a needless waste of typing precision to specify the return type Foo<Self> when you actually intend (and implement) those methods such that they return a Self.

Finally, note that you're using an F-bounded type variable which is not a complete and faithful replacement for a real Self type. (In particular, the pseudo-variable this is not known in the body of Foo to have type Self, and it isn't even known to have type Foo<Self>.) So you shouldn't expect everything that applies to true Self types to be applicable to an F-bounded type variable as well.

added
closed-as-intendedClosed as the reported issue is expected behavior
and removed
type-bugIncorrect behavior (everything from a crash to more subtle misbehavior)
triage-automationSee https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot.
on Feb 10, 2025
julien4215

julien4215 commented on Feb 10, 2025

@julien4215
Author

The advantage I found in using Foo<Self> instead of Self for the return type is that when you have an object of type Foo<Foo<dynamic>> and that you apply the method toggle you still get the type Foo<Foo<dynamic>>. With Self as the return type of toggle, you would get the type Foo<dynamic>.

I don't understand why it would be a needless waste of typing precision if it solves this problem.

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

    closed-as-intendedClosed as the reported issue is expected behavior

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @eernstg@julien4215@dart-github-bot

        Issue actions

          Self type leads to typing problems · Issue #60088 · dart-lang/sdk