Function parameters are not inferable when defined via JSDoc using @type tag (with strict)
🔎 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
@paramtag instead:
/**
* @param {string} param
*/
export function func(param) {};
yields FuncParam → [param: string]
- Using a union with
undefinedworks, but is not what I want for the definition offunc:
export function func(/** @type {string | undefined} */ param) {};
yields FuncParam → [param?: string | undefined]
- Trying the conditional without
inferworks (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.
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 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
I don't know why it's treated as optional, but the inference result is correct.
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)
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
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