Skip to content

Rule proposal: warn against unnecessary type parameters #3729

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
Zzzen opened this issue Aug 12, 2021 · 15 comments · Fixed by #8173
Closed

Rule proposal: warn against unnecessary type parameters #3729

Zzzen opened this issue Aug 12, 2021 · 15 comments · Fixed by #8173
Labels
accepting prs Go ahead, send a pull request that resolves this issue 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

Comments

@Zzzen
Copy link
Contributor

Zzzen commented Aug 12, 2021

https://github.com/microsoft/dtslint/blob/master/docs/no-unnecessary-generics.md

no-unnecessary-generics

Forbids a function to use a generic type parameter only once. Generic type parameters allow you to relate the type of one thing to another; if they are used only once, they can be replaced with their type constraint.

Bad:

function logAnything<T>(x: T): void;

Good:

function logAnything(x: any): void;

Bad:

function useLogger<T extends Logger>(logger: T): void;

Good:

function useLogger(logger: Logger): void;
@Zzzen Zzzen added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin triage Waiting for team members to take a look labels Aug 12, 2021
@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 Aug 16, 2021
@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-unnecessary-generics Rule proposal: warn against unnecessary generics May 3, 2022
@JoshuaKGoldberg JoshuaKGoldberg changed the title Rule proposal: warn against unnecessary generics Rule proposal: warn against unnecessary type parameters Dec 20, 2023
@JoshuaKGoldberg JoshuaKGoldberg self-assigned this Dec 20, 2023
@JoshuaKGoldberg
Copy link
Member

@danvk and I paired on an implementation today - it looks very doable! Especially encouraging given there are several re-implementations in the wild:

Assigning to myself to track that @danvk is planning on cleaning up the results of pairing a bit and sending a PR soon.

Aside: per @mattpocock's There Is No Such Thing As A Generic, the rule name we tentatively settled on is no-unnecessary-type-parameters.

@danvk
Copy link
Collaborator

danvk commented Dec 22, 2023

I spent some more time looking at this code. I think I see a good path forward but I have a TS API question.

I think the barebones implementation could be as simple as this:

const usage = tsutils.collectVariableUsage(tsNode.getSourceFile());
for (const typeParameter of tsNode.typeParameters) {
  const { uses } = usage.get(typeParameter.name)!;
  if (uses.length === 1) {
    context.report({
      data: {
        name: typeParameter.name.text,
      },
      node: parserServices.tsNodeToESTreeNodeMap.get(typeParameter),
      messageId: 'sole',
    });
  }
}

This is a very direct encoding of the rule: if a type parameter is only used once then it's bad.

Unfortunately, the usage map returned by tsutils.collectVariableUsage does not include inferred return types. So we'd have a false positive on this code:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

To get the inferred return type for that function I can do something like this:

const checker = parserServices.program.getTypeChecker();

const type = checker.getTypeAtLocation(tsNode);
const r = type.getCallSignatures().map(s => s.getReturnType())[0];

That definitely has the information I need to find the missing usage:

checker.typeToString(r)  // 'T[K]'
tsNode.typeParameters[0] === r.objectType.symbol.declarations[0] // true

So I'm wondering if there's a variation on collectVariableUsage that includes inferred return types? Or if there's some existing code to take a ts.Type and determine whether a type parameter is referenced anywhere inside it. Maybe @JoshuaKGoldberg knows the best path forward here.

Side note: why don't the wotan / eslint-plugin-etc rules have this issue? If I'm reading the implementations correctly, they're hard-wired to assume all type parameters are used if a function has an implementation (tsutils.isFunctionWithBody).

@JoshuaKGoldberg
Copy link
Member

wondering if there's a variation on collectVariableUsage that includes inferred return types?

I don't think such a thing exists, no. Maybe this is a good opportunity to add an option or fork into a more dedicated collectTypeUsage function or some such? (I haven't thought that through)

some existing code to take a ts.Type and determine whether a type parameter is referenced anywhere inside it.

This should be roughly doable with the scope manager.

Side note: why don't the wotan / eslint-plugin-etc rules have this issue?

...maybe they do have the issue? It'd be interesting to see a comparison of what edge cases are/n't caught by each rule...

@bradzacher
Copy link
Member

Yeah the only way to truly handle the case of an inferred return type is by using type information.

But all other cases can be handled using our scope manager as Josh mentioned - no need to dip into the TS AST or tsutils for that!

Question - are we just handling functions here? What about function types, method signatures and interface call signatures?

@Zzzen
Copy link
Contributor Author

Zzzen commented Dec 23, 2023

It would be quite limited if the rule only considers explicitly written return types, as many developers rely on TypeScript's type inference and do not explicitly specify return types in their code.

By including support for inferred return types, the rule would be able to accurately identify cases where type parameters are unnecessary and provide more comprehensive feedback to developers. This would greatly enhance the effectiveness and usefulness of the rule in real-world scenarios.

@bradzacher
Copy link
Member

With the upcoming isolatedDeclarations flag I'm sure that it'll become less and less common to rely so much on inferred return types :)

But definitely I do understand why you'd want a type-aware rule. But it does ofc come with the drawback of the rule being less used. There's a trade-off to both approaches.

@Josh-Cena
Copy link
Member

With the upcoming isolatedDeclarations flag

That flag is not going to be widely used, IMO. Many people use isolatedModules because there are many cases where you have to use a non-tsc compiler, such as testing and bundling, but I haven't witnessed significant need for a non-tsc declaration emitter, unless you have a truly gigantic codebase, but those represent a tiny proportion of our users or TS users in general.

@bradzacher
Copy link
Member

bradzacher commented Dec 24, 2023

I can see a lot of value in it for many reasons on any codebase experiencing slowness.
When TS compiles two projects that depend on one another (eg A depends on B) - TS builds B first (or checks that it's up-to-date) then builds A.
In even medium-sized codebases it can take a while to do TS builds (ours takes ~20s even with nx parallelisation) and people rely heavily on caching to try and speed things up. EG we rely upon nx to (a) cache builds and (b) manage the graph to remove duplicated efforts.

With isolatedDeclarations any codebase would be able to see massive wins by building .d.ts files with a much faster tool before then invoking TS to check things. With the artifacts already generated - one can then easily parallelise the work without regard for the dependency graph.

There's so much potential win here for most codebases to shave time off builds easily! Most companies will likely look to adopt this to improve build times and save CI $$$.
Once the tooling is built I guarantee that people will migrate. It may not be OSS repos that do so - but private repos will - and those represent a much larger portion of users than OSS repos

@Josh-Cena
Copy link
Member

Maybe I'm not in touch with this, but there should be nothing preventing people from building and using such tools today, maybe with another linter to report un-analyzable code (that relies on inference). Are there popular examples?

@bradzacher
Copy link
Member

There's nothing explicitly stopping - no. People could have devised their own set of rules and built their own transpilers to do this. But that is a heap of work that requires specialised knowledge so instead people have gone other more traditional routes like caching (see solutions like nx).

Once the standard is encoded in TS itself, then transpilers like SWC can build support and then users can reap the benefits for free.

@danvk
Copy link
Collaborator

danvk commented Jan 2, 2024

wondering if there's a variation on collectVariableUsage that includes inferred return types?

I don't think such a thing exists, no. Maybe this is a good opportunity to add an option or fork into a more dedicated collectTypeUsage function or some such? (I haven't thought that through)

I started down this road, check out
https://github.com/typescript-eslint/typescript-eslint/pull/8173/files#diff-eb9ab1f4e75e2315d4f312c2e3c3f18a1fd418281d5048c42434684746ce3499R109

This seems to work pretty well and I'm happy with all the passing tests. Since the collectTypeParameterUsage only needs to work with inferred types, its domain is somewhat limited. I don't think there's any way to infer the return of a type predicate, for example.

One thing I could use some pointers on: in order to apply this rule to methods in classes, I had to add a pretty broad selector:
https://github.com/typescript-eslint/typescript-eslint/pull/8173/files#diff-eb9ab1f4e75e2315d4f312c2e3c3f18a1fd418281d5048c42434684746ce3499R39-R40

Without that, the rule only runs for prop in this test (since it's an arrow function expression), not method:

declare class C<V> {
  method<T, U>(param: T): U;
  prop: <P>() => P;
}

MethodDefinition[typeParameters] didn't work but MethodDefinition did. Any guidance on how to refine this would be much appreciated! @JoshuaKGoldberg @bradzacher

I think we're getting pretty close to having this rule ready.

@JoshuaKGoldberg
Copy link
Member

MethodDefinition[typeParameters] didn't work but MethodDefinition did. Any guidance on how to refine this would be much appreciated!

Ah, this would be the MethodDefinition> the value property > typeParameters. So MethodDefinition[value.typeParameters].

@danvk
Copy link
Collaborator

danvk commented Jan 3, 2024

MethodDefinition[typeParameters] didn't work but MethodDefinition did. Any guidance on how to refine this would be much appreciated!

Ah, this would be the MethodDefinition> the value property > typeParameters. So MethodDefinition[value.typeParameters].

Adding this did get my rule to run but it wound up being a little too broad -- my rule wound up running twice on some method definitions. The narrower fix wound up being to add TSEmptyBodyFunctionExpression[typeParameters] to my list.

@JoshuaKGoldberg JoshuaKGoldberg added this to the 7.0.0 milestone Feb 1, 2024
@JoshuaKGoldberg
Copy link
Member

JoshuaKGoldberg commented Feb 1, 2024

Adding this to our v7 v8 milestone just to make sure it gets merged by then. It can definitely be merged into main before. 🚀

@JoshuaKGoldberg JoshuaKGoldberg removed their assignment Feb 20, 2024
@JoshuaKGoldberg
Copy link
Member

#8173 was merged into v8, and #9473 will backport that to main. We can mark this as done! 🚀

@JoshuaKGoldberg JoshuaKGoldberg removed this from the 8.0.0 milestone Jul 3, 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 Jul 13, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
accepting prs Go ahead, send a pull request that resolves this issue 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
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants