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

No implicit any error on call in JS but not in TS #55062

Closed
zaygraveyard opened this issue Jul 18, 2023 · 10 comments
Closed

No implicit any error on call in JS but not in TS #55062

zaygraveyard opened this issue Jul 18, 2023 · 10 comments
Assignees
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Needs Investigation This issue needs a team member to investigate its status.

Comments

@zaygraveyard
Copy link

Bug Report

πŸ”Ž Search Terms

js, overload

πŸ•— Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ entries but non apply.

⏯ Playground Link

JS version playground link with relevant code
TS version playground link with relevant code

πŸ’» Code

JS version:

/**
 * @template T
 * @template U
 * @typedef {T & { [K in Exclude<keyof U, keyof T>]?: never }} ButNot
 */

/**
 * @template T
 *
 * @overload
 * @param {ButNot<T, Function>} [context]
 * @returns {void}
 *
 * @overload
 * @param {(index: number) => any} mapFn
 * @param {T} [context]
 * @returns {void}
 */
function foo() {}

//   v- ❌ ERROR: Parameter 'k' implicitly has an 'any' type. (7006)
foo((k) => [k]);

// No errors βœ…
foo((k) => [k], undefined);

TS version:

type ButNot<T, U> = T & { [K in Exclude<keyof U, keyof T>]?: never };

function foo<T>(context?: ButNot<T, Function>): void;
function foo<T>(mapFn: (index: number) => any, context?: T): void;
function foo() {}

// No errors βœ…
foo((k) => [k]);

ButNot is as @RyanCavanaugh suggested in #4196 (comment).

πŸ™ Actual behavior

Parameter 'k' implicitly has an 'any' type. error in JS version but not TS one.

πŸ™‚ Expected behavior

No errors.

PS: This feels like a bug to me but I'm not sure if there's another way to achieve the expected outcome.

@IllusionMH
Copy link
Contributor

Do you see same behavior if you declare all types and overloads in TS, and only try use it in JS (instead of JS docs declaration)?
To exclude difference between JSDoc and TS declarations.

May be related to #39509

@zaygraveyard
Copy link
Author

Do you see same behavior if you declare all types and overloads in TS, and only try use it in JS (instead of JS docs declaration)?

Type declarations:

type ButNot<T, U> = T & { [K in Exclude<keyof U, keyof T>]?: never };

declare function foo<T>(context?: ButNot<T, Function>): void;
declare function foo<T>(mapFn: (index: number) => any, context?: T): void;

In JS:

// No errors βœ…
//   v- ❌ (parameter) k: any
foo((k) => [k]);

// No errors βœ…
//   v- βœ… (parameter) k: number
foo((k) => [k], undefined);

In TS:

// No errors βœ…
//   v- βœ… (parameter) k: number
foo((k) => [k]);

// No errors βœ…
//   v- βœ… (parameter) k: number
foo((k) => [k], undefined);

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jul 24, 2023
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 5.3.0 milestone Jul 24, 2023
@sandersn sandersn changed the title Picking wrong overload in JS but not in TS template tag only associates with last overload tag Jul 24, 2023
@sandersn sandersn changed the title template tag only associates with last overload tag template tag does not associate with first overload tag Jul 24, 2023
@sandersn
Copy link
Member

Experimenting with 3 overload signatures shows that only the first fails to get the type parameter. I'm not sure what's going on but at least I'm pretty sure it's not parsing.

Here's the variant I ended up with:

/**
 * @template T
 * @overload
 * @param {(index: number) => any} mapFn
 * @param {T} [context]
 * @returns {void}
 *
 * @overload
 * @param {Partial<T>} [bipartite]
 * @returns {void}
 *
 * @overload
 * @param {ButNot<T, Function>} [admissible]
 * @returns {void}
 */
function foo(admitation) {}

@sandersn
Copy link
Member

Confusingly, type baselines for this test case are correct. But the call still has the error from the OP:

// @checkJs: true
// @allowJs: true
// @strict: true
// @noEmit: true
// @filename: /overloadTag4.js
/**
 * @template T
 * @template U
 * @typedef {T & { [K in Exclude<keyof U, keyof T>]?: never }} ButNot
 */

/**
 * @template T
 * @overload
 * @param {ButNot<T, Function>} [admissible]
 * @returns {void}
 * @overload
 * @param {(index: number) => any} mapFn
 * @param {T} [context]
 * @returns {void}
 *
 * @param {any} admitation
 */
function foo(admitation) { !!admitation }
foo(x => [x])

@sandersn sandersn changed the title template tag does not associate with first overload tag No implicit any error on call in JS but not in TS Jul 25, 2023
@sandersn
Copy link
Member

I tested signature help some more, and it behaves the same in JS and TS--the first signature is instantiated and the others are not. Confusing but probably intentional.

Quick info is not good at specifying which signature was selected in a call, but it turns out both TS and JS select the same signature: the mapFn one. Both fail to make any inferences since context is not provided. The difference is that in JS a type parameter without inferences is constrained to any; in TS it's constrainted to unknown:

    function getDefaultTypeArgumentType(isInJavaScriptFile: boolean): Type {
        return isInJavaScriptFile ? anyType : unknownType;
    }

The inferred any causes a noImplicitAny error.

"strict": true and "checkJs": true is a combination that may lead to problems because checkJs needs to support very loose JS code, but also JS-in-TS code. In this case, JS-in-TS has its usability hurt by a loose-JS design decision.

@sandersn sandersn added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jul 25, 2023
@zaygraveyard
Copy link
Author

Thank you @sandersn for getting to the bottom of this issue.
How would you explain the outcome of the test in #55062 (comment) (TS infers correct type for callback parameter but not JS)?

@sandersn
Copy link
Member

In TS, T=unknown, so there's no implicit any error on k: unknown.
In JS, T=any, so there IS an implicit any error on k: any, since it was not explicitly declared.

In both cases, the same overload is chosen and there are no inference sources for T, so you're getting the default type parameter type for JS (any) vs TS (unknown).

@zaygraveyard
Copy link
Author

Well this is confusing because I observed TS inferring k: number not unknownπŸ˜…

@sandersn
Copy link
Member

Poking at it again, I see that T=any causes the first overload with ButNot to be chosen. You can force this to happen in Typescript with foo<any>(k => [k]). That way k's contextual type is any instead of number since mapFn isn't there to give index's type.

@zaygraveyard
Copy link
Author

I see, so if I understood correctly:

  • In JS the inference defaults to any so foo((k) => [k]) is equivalent to foo<any>((k) => [k]) (matching the ButNot overload)
  • In TS it defaults to unknown so foo((k) => [k]) is equivalent to foo<unknown>((k) => [k]) (matching the mapFn overload).

Which seams to holdup:

//        v- ❌ (parameter) k: any
foo<any>((k) => [k]);

//            v- βœ… (parameter) k: number
foo<unknown>((k) => [k]);

Playground link

Thank you for taking the time to explain it to me πŸ˜„

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

4 participants