TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

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

Open scottmcginness opened this issue 1 year ago • 5 comments

🔎 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

https://github.com/scottmcginness/TypeScript/commit/cda5366448cb994866517834e0c1ca78b10042cb

💻 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.

scottmcginness avatar May 19 '24 23:05 scottmcginness

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)

kungfooman avatar May 20 '24 09:05 kungfooman

@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.

fatcerberus avatar May 20 '24 14:05 fatcerberus

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 :sweat_smile:

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

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

(= after string)

kungfooman avatar May 20 '24 15:05 kungfooman

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

scottmcginness avatar May 20 '24 17:05 scottmcginness

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

Andarist avatar May 20 '24 22:05 Andarist