Skip to content

Self type leads to typing problems #60088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
julien4215 opened this issue Feb 10, 2025 · 3 comments
Closed

Self type leads to typing problems #60088

julien4215 opened this issue Feb 10, 2025 · 3 comments
Labels
closed-as-intended Closed as the reported issue is expected behavior

Comments

@julien4215
Copy link

julien4215 commented Feb 10, 2025

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

@dart-github-bot
Copy link
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.

@dart-github-bot dart-github-bot added legacy-area-analyzer Use area-devexp instead. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Feb 10, 2025
@eernstg
Copy link
Member

eernstg commented Feb 10, 2025

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.

@eernstg eernstg added closed-as-intended Closed as the reported issue is expected behavior and removed legacy-area-analyzer Use area-devexp instead. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. labels Feb 10, 2025
@eernstg eernstg closed this as completed Feb 10, 2025
@julien4215
Copy link
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
Labels
closed-as-intended Closed as the reported issue is expected behavior
Projects
None yet
Development

No branches or pull requests

3 participants