Skip to content

Full-signature types for JSDoc #1468

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

Merged
merged 7 commits into from
Jul 30, 2025
Merged

Conversation

sandersn
Copy link
Member

This PR allows @type tags to apply as a whole to a function. This is a returning Strada feature, but it's no longer implemented as a fallback lookup for a type tag. Instead, FunctionLikeBase now has a property WholeType that gets set in the reparser. If we can come up with Typescript syntax for this feature, it will work immediately--with a couple of caveats described below.

In the checker, the implementation is basically the same as in Strada, except that it refers to a property on FunctionLikeBase instead of looking up a tag. I put it on FunctionLikeBase because it is more convenient to retrieve, even though it only applies to FunctionLikeWithBodyBase. Notably, there was one check that I skipped because it short-circuits contextual typing in favour of the whole type, which I don't believe is correct--or is perhaps unobservable. When I removed it no tests changed.

I didn't check that declaration emit works, but I think there may be some tests that exercise it. In any case, since the signature's types are pulled from the whole-type when needed, and parameter/return types are supposed to be nil when it's present, I expect that emit will generate types correctly from the signature.

Another difference is that the parameter names of the resulting signature come from the type annotation, not the function declaration. This is consistent with something like

const x: number = 'xx'

where x: number when used even though it is initialised with a string, which of course is a string.

Speaking of errors, I improved the wording of the existing error and added another that forbids using @type and @param/@return. Both errors are worded in terms of JSDoc tags even though we could implement this feature in TS. We can revisit this either after coming up with a good name for this feature, or after we implement it in TS, if ever.

Open Questions

  1. What should this feature be called? Whole-function type annotation? Function annotation? TypeTag type annotation? This leads naturally to:
  2. What should the property on FunctionLikeBase be called?
  3. Can the errors be improved further, either the wording or the checks?
  4. (bonus) What would TS syntax for this look like?

Open Tasks

  1. Rename all the properties and functions to use the new terminology.

sandersn added 2 commits July 28, 2025 10:04
…s a returning Strada feature, but it's no longer implemented as a fallback lookup for a type tag. Instead, `FunctionLikeBase` now has a property `WholeType` that gets set in the reparser. If we can come up with Typescript syntax for this feature, it will work immediately--with a couple of caveats described below.

In the checker, the implementation is basically the same as in Strada, except that it refers to a property on FunctionLikeBase instead of looking up a tag. I put it on FunctionLikeBase because it is more convenient to retrieve, even though it only applies to FunctionLikeWithBodyBase. Notably, there was one check that I skipped because it short-circuits contextual typing in favour of the whole type, which I don't believe is correct--or is perhaps unobservable. When I removed it no tests changed.

I didn't check that declaration emit works, but I think there may be some tests that exercise it. In any case, since the signature's types are pulled from the whole-type when needed, and parameter/return types are supposed to be nil when it's present, I expect that emit will generate types correctly from the signature.

Another difference is that the parameter names of the resulting signature come from the type annotation, not the function declaration. This is consistent with something like
```ts
const x: number = 'xx'
```
where `x: number` when used even though it is initialised with a string, which of course is a string.

Speaking of errors, I improved the wording of the existing error and added another that forbids using `@type` and `@param/@return`. Both errors are worded in terms of JSDoc tags even though we could implement this feature in TS. We can revisit this either after coming up with a good name for this feature, or after we implement it in TS, if ever.

\#\# Open Questions
1. What should this feature be called?
2. Where should the code live?
3. Can the errors be improved further?
4. What would TS syntax for this look like?
@Copilot Copilot AI review requested due to automatic review settings July 28, 2025 17:18
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

-!!! error TS2345: Type 'undefined' is not assignable to type 'number'.
self("");
+ ~~~~
+!!! error TS2554: Expected 2 arguments, but got 1.
Copy link
Member Author

Choose a reason for hiding this comment

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

you can no longer make a parameter from a whole-type annotation optional by tacking on a nested /** @param [b] */, sorry, it's a horrible idea that should never have worked.

@@ -14,8 +14,8 @@
const tseslint = require('./typescript-eslint.js');
->tseslint : typeof tseslint
->require('./typescript-eslint.js') : typeof tseslint
+>tseslint : { config: (...configs: any[]) => void; }
+>require('./typescript-eslint.js') : { config: (...configs: any[]) => void; }
+>tseslint : { config: (...configs: import("./typescript-eslint.js").Config[]) => void; }
Copy link
Member Author

Choose a reason for hiding this comment

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

we now dereference the imported type instead of leaving it as typeof eslint.

+ ~~
+!!! error TS2702: 'NS' only refers to a type, but is being used as a namespace here.
+ ~~~~~~~~~~~~~~~
+!!! error TS8030: A JSDoc '@type' tag on a function must have a signature with the correct number of arguments.
Copy link
Member Author

Choose a reason for hiding this comment

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

our fancy span for the first error hurts us because the useless second error would get deduped if it used the whole span of the name.

sandersn added 2 commits July 29, 2025 07:39
1. Improve error spans on new/updated errors.
2. Improve check for new/updated errors.
3. Add (a lot of) code to emit type parameters in d.ts.

I'm not at all fluent with the declaration emitter code, so I need
careful review of it.
Thanks unified.js! Most complicated JSDoc I have ever seen.
@paoloricciuti
Copy link

I love this and i would love to have the same in TS. Could this be a potential syntax?

function my_function(){

} typed MyFunction;

this would require a new construct but it seems very legible

@jakebailey
Copy link
Member

Other than the one error positioning thing, I think this seems good.

The CHANGES.md file also needs an update, but I think that PR was never merged so that's not in this PR.

@sandersn
Copy link
Member Author

Yeah, I got sidetracked from updating CHANGES.md.
I'll take another look at the position tomorrow--yesterday I was convinced that TS would already work the new way in certain circumstances, but I need to double-check.

@sandersn sandersn changed the title Whole-assignment types for JSDoc Full-signature types for JSDoc Jul 29, 2025
@sandersn
Copy link
Member Author

@paoloricciuti I'm afraid that would be ambiguous with existing expression syntax.

Really what we should do is start a new issue to track this idea, but it sounds like the team has tried to come up with ideas in the past and failed to find any that more than 1 person likes.

@sandersn
Copy link
Member Author

I renamed WholeType to FullSignature based on ideas from the design meeting yesterday, so I think this is ready for final review. I decided to keep the new error span since it's closer to the intent of Typescript's span. Also I didn't want to write any more special-case code for functions (which may be how the TS code was arrived at as well)

@jakebailey
Copy link
Member

I decided to keep the new error span since it's closer to the intent of Typescript's span.

Can you explain what you mean by this?

I'm surprised by the current location just because the error goes on the type and not on the thing causing the error. It feel like if we had a function returning the wrong type, so put the error on the return type instead of the returned expression.

@sandersn
Copy link
Member Author

Yes. The Typescript code is OrElse(fn.Type(), fn), and the errors show up on the return type annotation. I think the closest thing with here is the full signature type annotation. I agree that it's strange for this particular error but it's better for the other 3. I think it was OK by mistake in Strada but when I weighed the time needed to figure out the special case code to be equivalent versus following the intent of the (simpler) TS code path, I decided on the simpler way.

@jakebailey
Copy link
Member

I think you might misunderstand my example? I'm comparing this PR's behavior to if you wrote:

function foo(): number {
    return "Hello!";
}

And the error were placed on number and not on the return expression, where the bug is.

I suppose if this PR had "real syntax", like:

function foo() :: Hmm {
    return "Hello!";
}

And the func were missing a parameter Hmm asks for or something, it'd be questionable where the error goes, but I just find it odd to have:

/** @type {Hmm} */
function foo() {
    return "Hello!";
}

And have the error appear on Hmm, when it's the function that would need to change?

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

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

Implementation wise, this seems fine, though.

The only other thing I was wondering is if we really need this on FunctionLikeData and not just a specific node or two (does this actually work on accessors? Is it tested?), but it's probably okay.

@sandersn
Copy link
Member Author

it is supposed to work on accessors; I don't know if we have test coverage for it.

@sandersn sandersn added this pull request to the merge queue Jul 30, 2025
Merged via the queue into microsoft:main with commit 008942f Jul 30, 2025
22 checks passed
@sandersn sandersn deleted the whole-ass-type branch July 30, 2025 18:16
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

Successfully merging this pull request may close these issues.

3 participants