Skip to content
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

Function parameters are not inferable when defined via JSDoc using @type tag (with strict) #58580

Open
scottmcginness opened this issue May 19, 2024 · 5 comments
Labels
Help Wanted You can do this Possible Improvement The current behavior isn't wrong, but it's possible to see that it might be better in some cases
Milestone

Comments

@scottmcginness
Copy link

🔎 Search Terms

infer
@type
strict
JSDoc
Parameters
Function

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about type inference.

I've pulled the latest version of this repository and made a test case in the "fourslash" section, which demonstrates the problem I see.
(I couldn't see how to do something like this in the playground, sorry. Commit is linked)

⏯ Playground Link

scottmcginness@cda5366

💻 Code

// In file func.js
export function func(/** @type {string} */ param) {};
// In file use-it.js
import { func } from "./func.js";
type FuncParam = (typeof func) extends (...args: infer P) => any ? P : never;
//   ^ never, but expected [param: string]

In tsconfig.json:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": true
  }
}

(The above definition is obviously just Parameters<T>, but put in full for comparison with another example below)

🙁 Actual behavior

The type FuncParam resolved to never (i.e. it took the false branch of the ternary)

🙂 Expected behavior

The type FuncParam should be [param: string], as given by the JSDoc @type tag

Additional information about the issue

This seems like a bug because all other ways of specifying the func function with JSDoc or the FuncParam type seemed to work as expected:

  • Using the @param tag instead:
/**
 * @param {string} param
 */
export function func(param) {};

yields FuncParam[param: string]

  • Using a union with undefined works, but is not what I want for the definition of func:
export function func(/** @type {string | undefined} */ param) {};

yields FuncParam[param?: string | undefined]

  • Trying the conditional without infer works (but is not usable for the intended purpose):
import { func } from "./func.js";
type FuncIsAFunc = (typeof func) extends (...args: any) => any ? 'good' : never;
//   ^ 'good'
  • Pulling just the first parameter out also works (but obviously that's not how Parameters<T> works):
import { func } from "./func.js";
type FuncParam = (typeof func) extends (arg: infer A) => any ? A : never;
//   ^ string

This also only seemed to occur specifically with strict: true. I couldn't see this with any of the other strict... options (though I may have missed something here)


All other views on the function seems to show that it is happily a function with a string parameter. i.e. the tooltip hover over func, while inside use-it.ts shows:

(alias) function func(param: string): void
import func

The output I see from the single test linked above (using hereby runtests --tests=jsDocInferredFunctionParameters) is:

  1) fourslash tests
       tests/cases/fourslash/jsDocInferredFunctionParameters.ts
         fourslash test jsDocInferredFunctionParameters.ts runs correctly:

      AssertionError: At marker '': quick info text: expected 'type FuncParam = never' to equal 'type FuncParam = [param: string]'
      + expected - actual

      -type FuncParam = never
      +type FuncParam = [param: string]

      at _TestState.verifyQuickInfoString (src\harness\fourslashImpl.ts:1863:16)
      at Verify.quickInfoIs (src\harness\fourslashInterfaceImpl.ts:268:20)
      at eval (jsDocInferredFunctionParameters.js:13:8)
      at runCode (src\harness\fourslashImpl.ts:4618:9)
      at runFourSlashTestContent (src\harness\fourslashImpl.ts:4576:5)
      at runFourSlashTest (src\harness\fourslashImpl.ts:4559:5)
      at Context.<anonymous> (src\testRunner\fourslashRunner.ts:59:39)
      at processImmediate (node:internal/timers:476:21)

This also happens for arrow functions and class methods, e.g.

// In func.js
export const func = (/** @type {string} */ param) => {};
export class Cls{
  method(/** @type {string} */ param) {}
}

with similar code as for FuncParam.

@kungfooman
Copy link

Funny behaviour, and why does it work somewhat better in only one file: Playground

function func(/** @type {string} */ param) {}
/**
 * @typedef {typeof func} Func
 * @typedef {Func extends (...args: infer P) => any ? P : never} FuncParam
 */

Result: type FuncParam = [param?: string] (it shouldn't be optional)

@fatcerberus
Copy link

@kungfooman That's because the parameter itself is, in fact, optional:

function func(/** @type {string} */ param) {}
/**
 * @typedef {typeof func} Func
 * @typedef {Func extends (...args: infer P) => any ? P : never} FuncParam
 */

func();  // not an error

Playground link

I don't know why it's treated as optional, but the inference result is correct.

@kungfooman
Copy link

I don't know why it's treated as optional, but the inference result is correct.

If we turn on strictNullChecks it's suddenly never again 😅

IMO it should only ever be considered optional if we use the Google Closure syntax:

function func(/** @type {string=} */ param) {}

(= after string)

@scottmcginness
Copy link
Author

Thank you for the playground links! Reproduces the problem nicely.

In the non-optional cases above, the tooltip hover still thinks that func is a function with a non-optional (param: string) parameter. Which seems correct. Later still having param be optional seems weird to me too, but maybe that's how optionality works 🤷

In addition, both the infer and func() remain inconsistent with how @param works (which is how I expect, in fact):

/** @param {string} param */
function func(param) {}
func() // error

and FuncParam as above is [param: string]. Playground link

@Andarist
Copy link
Contributor

The generated function has SignatureFlags.IsUntypedSignatureInJSFile assigned to it. So its min argument count gets computed as 0. Based on that calls without any arguments are allowed as the provided number of arguments (0 here) satisfies the min argument count.

Currently, this flag is not assigned to signatures coming from JS files when the function has either @param tag or when the function itself is typed using @type. For that reason, you end with the same problem when the function is contextually-typed using @satisfies:

/** @satisfies {(arg: string) => void} */
const func = function func(param) { };
func() // oops

Related(ish) case to the @satisfies can be found here

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Possible Improvement The current behavior isn't wrong, but it's possible to see that it might be better in some cases labels Jun 17, 2024
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jun 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this Possible Improvement The current behavior isn't wrong, but it's possible to see that it might be better in some cases
Projects
None yet
Development

No branches or pull requests

5 participants