Skip to content

Conversation

@sterliakov
Copy link
Contributor

@sterliakov sterliakov commented May 8, 2025

Summary

This PR significantly changes handling of top-level const, let and var variables by useExplicitType nursery rule.

Fixes #5932.

I went with the following heuristics:

  • Any literal value and primitive is trivial, except for let and var bindings with literal null or undefined RHS as those don't usually have real null or undefined type but are later reassigned to something more useful.
  • Any cast of the shape x as SomeType and legacy-style <SomeType>x is also trivial.
  • Trivial object literals are also fine. An object literal is trivial if all its non-method properties are trivial. This does not include methods and getters: they are checked separately and will be flagged anyway.
  • Any var, let or const declaration of a variable without an explicit type annotation is flagged by this rule unless the RHS is trivial.

Going down this rabbit hole,

  • Any inline callback function that is used as a part of call expression can be untyped (no argument types, no return type)

The rules outlined above solve one of the existing pain points. The following code has one missing type, and yet current biome version reports two errors on it (for the variable and for the method).

const x = {
    fn(arg): void {
        console.log("this is untyped")
    }
}

Test Plan

I added several simple testcases and will expand that list as the PR grows.

@sterliakov sterliakov marked this pull request as draft May 8, 2025 22:55
@github-actions github-actions bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels May 8, 2025
@sterliakov sterliakov changed the title Rewrite useExplicitType top-level variables checking fix(useExplicitType): rewrite top-level variables checking May 8, 2025
@sterliakov sterliakov marked this pull request as ready for review May 13, 2025 11:37
@sterliakov
Copy link
Contributor Author

QQ: typescript-eslint allows untyped setters. Should we add an option to optionally prohibit untyped setters? That's IMO quite a counter-intuitive behavior and explicit hints help define the public interface much better.

E.g. this passes cleanly:

const x = {
    get foo(): number { return 1; }
    set foo(newFoo) { console.log(newFoo); }

I believe that set should produce a diagnostic. That probably needs a separate option to stay closer to ESLint and is out of scope of this PR, but I'd be glad to contribute such addition in a follow-up patch.

@ematipico
Copy link
Member

Should we add an option to optionally prohibit untyped setters?

I believe we should enforce a type on setters too.

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some snapshots changed too much and needs to be addressed

@codspeed-hq
Copy link

codspeed-hq bot commented May 16, 2025

CodSpeed Performance Report

Merging #5935 will not alter performance

Comparing sterliakov:bugfix/gh-5932-use-explicit-type (a0a385c) with main (d5b4011)

Summary

✅ 95 untouched benchmarks

@sterliakov
Copy link
Contributor Author

sterliakov commented May 18, 2025

So many questions. I'm fine with checking setter types, but the rule seems still horribly broken... Currently the following is flagged as error:

arr.map((item) => item + 1);
new Promise((resolve) => resolve(1));

and that's even checked in tests. Usually such inline functions are left untyped; I expect a lot of projects using a line similar to the above without annotating resolve or item explicitly. Does it make sense to require annotation on such callbacks? Typesript infers their types backwards, and such lambdas tend to be short and clear without annotations...

return None;
}

// TODO: why only arrow functions are ignored inside typed return?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll point this out in a review comment as it really makes no sense to me. Why should namedFunc and arrowFunc be handled differently (namedFunc rejected, arrowFunc allowed) in the following case?

interface Behavior {
  attribute: string;
  namedFunc: () => string;
  arrowFunc: () => string;
}

function getObjectWithFunction(): Behavior {
  return {
    namedFunc: function myFunc() { return "value" },
    arrowFunc: () => {},
  }
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good point, unfortunately, we miss some historical facts. Perhaps you could bring this up in the original issue and continue the discussion there.

@sterliakov
Copy link
Contributor Author

I hope this is ready for another round. Does this update warrant a .changeset entry?

@dyc3
Copy link
Contributor

dyc3 commented May 22, 2025

This rule was present before 2.0 right? If that is the case, then yes, it should have a changeset.

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @sterliakov !

@ematipico ematipico merged commit 6c4e24c into biomejs:main May 30, 2025
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

💅 False positive lint/nursery/useExplicitType on module-level assignment when RHS is not a constant

3 participants