Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Rule proposal: no-inferrable-types equivalent for function return types #2673

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
vegerot opened this issue Oct 13, 2020 · 29 comments
Closed
Labels
enhancement: new plugin rule New rule request for eslint-plugin locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look

Comments

@vegerot
Copy link
Contributor

vegerot commented Oct 13, 2020

tl;dr I would like a rule to ban inferable function return types, similar to how no-inferrable-types acts on variables.

  • [x ] I have tried restarting my IDE and the issue persists.
  • [x ] I have updated to the latest version of the packages.
  • [ x] I have read the FAQ and my problem is not listed.
{
  "rules": {
    "@typescript-eslint/no-inferrable-types": ["error"],
    "@typescript-eslint/explicit-function-return-types": ["error"],
  }
}

Expected Result

If I had

{
  "rules": {
    "@typescript-eslint/no-inferrable-types": ["error"],
    "@typescript-eslint/no-inferrable-function-types": ["error"],
  }
}

I would expect all inferable types to be banned

//err
const a: number = 1
const b = 1

//err
function c(): number {return 1}
function d() {return 1}

Actual Result
Instead, since that second rule that doesn't exist, I'm stuck with

//err
const a: number = 1
const b = 1

function c(): number {return 1}
//err
function d() {return 1}

Additional Info

I would like a rule to ban inferable function return types, similar to how no-inferrable-types acts on variables.

Versions

package version
@typescript-eslint/eslint-plugin latest
@typescript-eslint/parser latest
TypeScript latest
ESLint latest
node latest
@vegerot vegerot added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Oct 13, 2020
@bradzacher bradzacher changed the title [no-inferrable-types] Equivalent for [explicit-function-return-types] new rule no-inferrable-types equivalent for function return types Oct 13, 2020
@bradzacher bradzacher added enhancement: new plugin rule New rule request for eslint-plugin and removed triage Waiting for team members to take a look labels Oct 13, 2020
@JoshuaKGoldberg JoshuaKGoldberg added the accepting prs Go ahead, send a pull request that resolves this issue label Oct 25, 2021
@bradzacher bradzacher changed the title new rule no-inferrable-types equivalent for function return types Rule proposal: no-inferrable-types equivalent for function return types May 3, 2022
@Morozzko
Copy link

Morozzko commented Sep 4, 2023

someone have this settings work?

@vegerot
Copy link
Contributor Author

vegerot commented Sep 5, 2023

I opened this issue when I was a naive child. I now see the error of my ways.

@rentalhost
Copy link

@vegerot I believe it is still a valid addition. Why do you think this would be a bad idea now? I know there are cases where the type needs to be explicit for the return to be correctly 'filtered.' But there are straightforward cases, like the ones you mentioned, that would be great. Less clutter in the code for obvious things.

@norflin321

This comment was marked as off-topic.

@bradzacher

This comment was marked as off-topic.

@norflin321

This comment was marked as off-topic.

@vegerot
Copy link
Contributor Author

vegerot commented Mar 14, 2024

I opened this issue when I was young and dumb. I would never use such a rule

@bradzacher

This comment was marked as off-topic.

@Morozzko

This comment was marked as off-topic.

@norflin321

This comment was marked as off-topic.

@bradzacher

This comment was marked as off-topic.

@anasbud
Copy link

anasbud commented Jul 17, 2024

The rule shouldn't be about preventing you from specifying the return type. It's supposed to prevent you from explicitly writing what Typescript already knows.

@kirkwaiblinger
Copy link
Member

@anasbud

The trouble is... TS will basically always come up with a return type, no matter what you return. It's just the union of whatever you return. For example,

function foo() {
   if (Math.random() < 0.5) {
      return 'string';
   } else {
      return 42;
   }
}

foo;
//^? function foo(): "string" | 42

So, "prevent you from explicitly writing what Typescript already knows" implies no explicitly specified return types, ever.

Whether or not one prefers or dislikes inferred return types (disclaimer - I prefer them always to be explicit), one would need to come up with some understandable criteria that ban some but not all return type annotations, in order for this issue to be actionable. No such criteria have been proposed in this thread yet.

Perhaps someone would like to have a stab at it? If not, I think we should close this issue.

@Josh-Cena
Copy link
Member

Josh-Cena commented Jul 17, 2024

@kirkwaiblinger That means the rule would error if you write : "string" | 42 as the return type, and no error if you write : string | number, or type Answer = "string" | 42 and : Answer.

I think this rule has a lot of value in itself; I'm a big +1 as a user, though not sure about complexity.

@kirkwaiblinger
Copy link
Member

@Josh-Cena

I see.... so you're thinking "If the return type is the same as what would have been inferred, ban it. Otherwise allowed."?

@JoshuaKGoldberg
Copy link
Member

JoshuaKGoldberg commented Aug 10, 2024

Coming over from #9764 (comment): now that we can use isTypeAssignableTo (#7936), is there any reason to limit this to same? Is there any reason not to expand it to same or wider?

// I would hope this would error?
function giveText(): string {
  return 'abc' as const;
}

@Josh-Cena
Copy link
Member

No because it would be intended.

function mayReturnStringOrNumber(): string | number {
  return "Currently is just a constant: will return number later";
}

Here, if the rule forces you to remove the type, then later the addition of number would be a breaking change.

I don't think we can even use "X is assignable to Y and Y is assignable to X" without shooting down a lot of valid code. For exmaple, arr.map((x): SomeComplexObjectType => ({ ... })) is arguably much better since you get a nicely aliased SomeComplexObjectType[] return type instead of a huge object literal type.

@bradzacher
Copy link
Member

Honestly I'm a -1 on this rule because it's so vague.
I don't think there's any world where you can define the rule such that it would be truly predictable and work without surprises.

For example

interface T { a: string }
function foo(arg: string): T {
  return { a: arg };
}

Should this report?
Technically yes because the object type { a: string } is the same as the type T.
But also no it shouldn't because { a: string } is an anonymous type and T is a named type -- so they're also not the same thing.

This applies to things like

type Path = string
function asPath(arg: string): Path {
  if (someComplicatedLogicIsFalse()) {
    return coerceArgToPath(arg);
  }
  return arg;
}

Should the rule report here? Well maybe in future we wanted to make Path a branded type but the rule forced us to remove annotations.

I just think there are so many cases that are ambiguous and you would end up with surprising results.

Additionally in FOUR YEARs this request has garnered just 8 unique reactions.
There's just not a community want for this rule.
The OP has even stated that since they filed this they've changed their mind and wouldn't use this rule.

So yeah, big -1 from me.

@bradzacher bradzacher added triage Waiting for team members to take a look and removed accepting prs Go ahead, send a pull request that resolves this issue labels Aug 25, 2024
@kirkwaiblinger
Copy link
Member

Personally, my thoughts here align with @bradzacher's

@bradzacher
Copy link
Member

bradzacher commented Aug 25, 2024

@Josh-Cena is a +1, so that leaves @JoshuaKGoldberg.

@JoshuaKGoldberg
Copy link
Member

What if we made the rule only report if:

  • each constituent of the inferred return type (from returns) === the declared return type
  • no type aliases (e.g. type Path = string) are used

The former point solves for wider declarations than inferred types (#2673 (comment), : string | number) and interfaces vs. object literals (#2673 (comment) part one, interface T). The latter solves for #2673 (comment) part two (type Path).

I still think there's value in a rule to flag flagrantly unnecessary return types. e.g.:

function hasBooleanMatch(values: boolean[]): boolean {
  return values.some(Boolean);
}

@bradzacher
Copy link
Member

bradzacher commented Aug 26, 2024

In this function, typeof (typeof v === 'boolean') === boolean and the return type is boolean

function isBoolean(v: unknown): boolean {
  return typeof v === 'boolean';
}

But if you remove the return type now the return type is inferred as v is boolean, changing the return type and potentially causing downstream type errors.

See specialisation of this rule in #9764


declare function querySelector<T>(selector: string): T;
interface MyNode { type: 'node' }

function myFunction1(): MyNode {
  return querySelector('node');
}
function myFunction2() {
  //       ^?
  return querySelector('node');
}

playground
In this example:
myFunction1's explicit return type is MyNode and typeof querySelector('node') is inferred to be MyNode -- so the rule will report. However as illustrated by myFunction2 -- without the annotation the return type is instead inferred to be unknown.

This is a surprising result that we've similarly seen from no-unnecessary-assertion #3310


function myFunction1(): string {
  return 'a';
}
function myFunction2(): 'a' {
  return 'a';
}
function myFunction3() {
  //     ^?
  return 'a';
}

playground

Which annotation is "the most correct"? Both are correct and technically by definition both are inferable.


My point:
There is large complexity to the rule and all the edge cases.
It's definitely possible for us to spec something out and we could likely get something reasonably predictable given enough time and documentation.

But in 4 years we haven't even had TEN community interactions with this issue.
So it's not something the community is interested in using.

Hence my conclusion is that the value is much, much lower than the very high cost of implementation and what will likely be a high cost of maintenance (at least in the short term as we iron out the documentation and edge cases).

@Josh-Cena
Copy link
Member

But if you remove the return type now the return type is inferred as v is boolean, changing the return type and potentially causing downstream type errors.

Before inferred predicates, it would error if you annotated : boolean and not error if you annotated : v is boolean. Afterwards, it would be the other way. (As I said above, it would error as long as the types are not nominally equivalent.) This is strictly better than a rule that always enforces type annotations because it surfaces any changes in the inferrable types.

This is a surprising result that we've similarly seen from no-unnecessary-assertion

I agree it's surprising but we already have similar behavior. I don't think we should not build a rule just because of certain limitations that users are already aware of.

Which annotation is "the most correct"? Both are correct and technically by definition both are inferable.

That depends on what TS thinks? If TS infers 'a' then we report : string and vice versa.

@bradzacher
Copy link
Member

That depends on what TS thinks? If TS infers 'a' then we report : string and vice versa.

Well funny story cos the answer is both! Or rather "either depending on the context of the return value".

declare const x: "a";
function f() {
    //   ^? () => "a"
    return x;
}
function g() {
    //   ^? () => string
    return "a";
}

This surprising nature is the sort of logic that we would need to encode into the rule. Note that it is not evidently clear from the type objects alone that this is the case and instead we would need to do syntactic checks.

This is the sort of case that can seem surprising and buggy to users.

I agree it's surprising but we already have similar behavior.

Similar, buggy behaviour that we just haven't gotten around to fixing yet. A new rule would be required to not have the same bug given it's well known, of course.


My comment was just intended to be some examples of hairy cases. There will be many more that would be uncovered through implementation and more still through initial user testing.

I just don't think the user interest in such a rule existing (a small percentage going by the community interactions) is large enough to outweigh the likely large cost of design, implementation and support for such a rule.

Either way - I've said my piece. I'm a strong -1 and there isn't anything that is going to convince me otherwise barring a overwhelming outpouring of support from the community in favour of it.

@Josh-Cena
Copy link
Member

If it's significantly more difficult than "ask TS what would the inferred type be if there's no type annotation" then I also don't think there's significant ROI. I would love to be a user of this rule though.

@bradzacher
Copy link
Member

bradzacher commented Aug 26, 2024

If it's significantly more difficult than "ask TS what would the inferred type be if there's no type annotation"

This is not possible. TS cannot do speculative querying at all and instead we can only ask it for the types based on the code as it is written.
Which means we can have to manually make educated guesses based on the rules we know are encoded into TS's type system.

So that means we must query the functions control flow graph to collect all its termination points so that we can guess at what TS will use as the return type for each one, then we can manually recreate the resulting union type and compare it against the annotated return type.

Which is a whole thing that I haven't even gone into or even done before in a rule -- control flow analysis is complicated. All the examples we've been looking at are simple zero branch, single return functions.

But like here's the next level of complexity

function foo(b: boolean): string | number {
  if (b) {
    return 1;
  }
  return 'a';
}

Then there's throw and never returning function calls, assertion functions to consider.

At a high level this rule would be:

  1. if a function has an explicit return type annotation
  2. using its control flow graph, collect all of its valid return points
  3. for each return point guess the calculated type for the return and construct a union type by hand
  4. compare this with the real annotation and determine, based on our "pre-defined rules", whether or not the annotation is "bad"

Step 2 we've never done before.
Step 3 will be a doooooozy.
Step 4 is what we've mostly discussed and is key in making the rule predictable and un-surprising

@JoshuaKGoldberg
Copy link
Member

...at this point I'm fine with dropping the issue. I still would really really like to have the rule exist, but Brad's points on the difficulties of it have scared me away from wanting to tackle it in typescript-eslint. 😣

@Josh-Cena
Copy link
Member

^Exactly same thought.

@JoshuaKGoldberg
Copy link
Member

We have consensus.

To anybody reading this and feeling πŸ‘Ž on the decision: we're not denying that the rule can exist! Just that we, as a community open source project, have nowhere near enough time to maintain it. You're welcome to use the docs in https://typescript-eslint.io/developers/custom-rules to build the rule yourself.

In fact, I'd request someone please build this rule & prove us wrong. It'd be a great one to have exist. πŸ˜„

Thanks all!

@JoshuaKGoldberg JoshuaKGoldberg closed this as not planned Won't fix, can't repro, duplicate, stale Aug 27, 2024
@github-actions github-actions bot added the locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. label Sep 4, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement: new plugin rule New rule request for eslint-plugin locked due to age Please open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing. package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look
Projects
None yet
Development

No branches or pull requests

9 participants