Skip to content

Inference should use generic bounds to infer argument types #1761

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
natebosch opened this issue Jul 27, 2021 · 8 comments
Closed

Inference should use generic bounds to infer argument types #1761

natebosch opened this issue Jul 27, 2021 · 8 comments

Comments

@natebosch
Copy link
Member

I generally don't expect that specifying a generic which exactly matches what would be inferred should impact behavior, but it changes how arguments are inferred.

For example

void something<T extends Object>(void Function(T) c) {}

void main() {
  something<Object>((arg) {});
  something((arg) {});
 }

In both cases the generic T takes the value Object, however in the latter case the arg has an inferred type of Object? instead of Object.

cc @leafpetersen

@leafpetersen
Copy link
Member

Related to #1194 .

@Levi-Lesches
Copy link

In both cases the generic T takes the value Object, however in the latter case the arg has an inferred type of Object? instead of Object.

Am I missing something? The following compiles just fine, and I think Dart won't infer T from the callback I pass. How else could I check the inferred type of arg?

void something<T extends Object>(void Function(T) c) { }

void main() {
  something<Object>((Object arg) {});
  something((Object arg) {});
 }

@lrhn
Copy link
Member

lrhn commented Jul 28, 2021

So, we don't have a downward inference hint for T because there is no type argument. That makes (arg) {} is inferred without a hint, giving Object? for arg. Then we infer T to be Object on the way back, because it's not constrained by the arg type and we can just instantiate to bounds.
In reality, we did have some hint to the value of T because it has a bound, and we're just not using it.

If I change the declaration to <T extends num>, I still get arg being Object?, but T being bound to num.

void something<T extends num>(void Function(T) c) {
  print("T = $T, c: ${c.runtimeType}");
}

void main() {
  something<int>((arg) {}); // T = int, c: (int) => void
  something((arg) {});      // T = num, c: (Object?) => void
 }

I'm not sure what would happen if we used the bound type as the type of T during inference of (arg){}. It would likely become (Object arg) {} (or (num arg) {} in my example), then we would try to infer T back from that and get the same thing, which is fine.
Can't really guess whether there are places where it would make a bigger difference, or break things.

@natebosch
Copy link
Member Author

The motivating example is trying to write the equivalent of a catch with rethrow on a Future using onError.

It must be written as

.onError<Object>((error, stackTrace) {
  // some cleanup
  throw error;
});

Without specifying the <Object> (or specifying on the function argument (Object error, stackTrace) {) you'll get the error The type 'Object?' of the thrown expression must be assignable to 'Object'.

@eernstg
Copy link
Member

eernstg commented Jan 17, 2024

Here's another issue that seems to call for a similar feature: #3567.

@leafpetersen
Copy link
Member

I filed an issue with a description of the changes to the meta-theory that I believe would be required to improves this here.

@dgreensp
Copy link

dgreensp commented Apr 4, 2024

Possibly related: It would be helpful if function argument types could be inferred when the context is a type parameter with a function bound:

void main() {
  final func = wrap((x, [String? y]) {}); // type omitted on x; would ideally be `int` but is `dynamic`
  func(1, "a");
  print(func.runtimeType); // (dynamic, [String?]) => Null
  func("x", "y"); // no error :(
}

T wrap<T extends void Function(int)>(T t) => t;

The equivalent code works fine in TypeScript FWIW:

function main() {
  const func = wrap((x, y?: string) => {}); // x inferred to be `number`
  func(1, "a");
  func("x", "y"); // error: string is not assignable to number
}

function wrap<T extends (n: number) => void>(t: T): T {
  return t;
}

The motivation is not having to specify types over and over for defining a set of related functions (e.g. factories with slightly different sets of arguments); it would be nice if this gave suitable types to a, b, and c instead of making them dynamic:

T wrap<T extends void Function(int, String, bool)>(T t) => t;

final func1 = wrap((a, b, c, [int? d]) { /* ... */ });
final func2 = wrap((a, b, c, {String? e}) { /* ... */ });
// ...

Slightly off topic, but the TypeScript "satisfies" keyword that can be used for a similar purpose (namely, influencing inference of function argument types without setting the context type):

type MyFunction = (a: number, b: string, c: boolean) => void;

const func1 = ((a, b, c, d?: number) => { /*...*/ }) satisfies MyFunction;
const func2 = ((a, b, c, e?: string) => { /*...*/ }) satisfies MyFunction;

@eernstg
Copy link
Member

eernstg commented Apr 5, 2024

I suspect that the inferred types of formal parameters of a function literal are handled in a slightly different way. I created #3695 to discuss a possible improvement in this area.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants