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

fix(createEventHook): type check for multiple arguments #4555

Merged

Conversation

isimehmeti
Copy link
Contributor

@isimehmeti isimehmeti commented Jan 31, 2025

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.
⚠️ Slowing down new functions

Warning: Slowing down new functions

As the VueUse audience continues to grow, we have been inundated with an overwhelming number of feature requests and pull requests. As a result, maintaining the project has become increasingly challenging and has stretched our capacity to its limits. As such, in the near future, we may need to slow down our acceptance of new features and prioritize the stability and quality of existing functions. Please note that new features for VueUse may not be accepted at this time. If you have any new ideas, we suggest that you first incorporate them into your own codebase, iterate on them to suit your needs, and assess their generalizability. If you strongly believe that your ideas are beneficial to the community, you may submit a pull request along with your use cases, and we would be happy to review and discuss them. Thank you for your understanding.


Description

This PR improves the typing of createEventHook to correctly support multiple arguments when used with tuple types. Previously, tuple types were treated as a single argument instead of separate parameters.

Before (Issue)

const eventHook = createEventHook<[string, number]>();
eventHook.on((foo, bar) => {  // ❌ Incorrect: foo is [string, number], bar is unknown
  console.log(foo, bar);
});
  • The foo parameter was inferred as [string, number] instead of string, and bar was inferred as unknown.

After (Fix)

const eventHook = createEventHook<[string, number]>();
eventHook.on((foo, bar) => {  // ✅ Correct: foo is string, bar is number
  console.log(foo, bar);
});
  • Now, multiple parameters are properly inferred when passing a tuple type.

Additional context

-The fix introduces an extra condition:

[T] extends [any[]] ? (...param: T) => void 

This ensures that tuples are spread as separate arguments instead of being treated as a single parameter.

  • The change is backward-compatible and does not affect single-parameter usage.

@dosubot dosubot bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Jan 31, 2025
OrbisK
OrbisK previously approved these changes Jan 31, 2025
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Jan 31, 2025
ilyaliao
ilyaliao previously approved these changes Feb 1, 2025
@autofix-ci autofix-ci bot dismissed stale reviews from OrbisK and ilyaliao via 58bfb54 February 3, 2025 08:45
OrbisK
OrbisK previously approved these changes Feb 3, 2025
ilyaliao
ilyaliao previously approved these changes Feb 3, 2025
@ilyaliao
Copy link
Member

ilyaliao commented Feb 3, 2025

Should we care about the workflows? Or can we merge directly? @OrbisK

@OrbisK
Copy link
Collaborator

OrbisK commented Feb 3, 2025

Should we care about the workflows? Or can we merge directly? @OrbisK

image

looks like an typecheck issue (https://app.netlify.com/sites/vueuse/deploys/67a08230f106910008a97622). So we should care.

@isimehmeti
Copy link
Contributor Author

@OrbisK

the test that fails is this one

  it('should pass union type', () => {
    let count = 0

    const { on: onResult, trigger } = createEventHook<number | string>()

    // union type should be inferred
    onResult(value => count = 2)
    trigger(1)
    trigger(2)

    expect(count).toBe(2)
  })

Adding
T extends any[] ? (...param: T) => void

makes the type more complex so in order to interfere union types it should be declared
createEventHook<[number | string]>()

we should find a common ground for this case

@isimehmeti isimehmeti dismissed stale reviews from ilyaliao and OrbisK via a4b0356 February 3, 2025 09:09
@dosubot dosubot bot added size:S This PR changes 10-29 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels Feb 3, 2025
@isimehmeti isimehmeti requested review from ilyaliao and OrbisK February 3, 2025 10:09
ilyaliao
ilyaliao previously approved these changes Feb 3, 2025
- Undo previous changes for union types
- Fix type inference for union types in Callback<T>
@dosubot dosubot bot added size:XS This PR changes 0-9 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Feb 4, 2025
@isimehmeti isimehmeti marked this pull request as draft February 4, 2025 09:58
@isimehmeti isimehmeti marked this pull request as ready for review February 4, 2025 10:09
@isimehmeti
Copy link
Contributor Author

isimehmeti commented Feb 4, 2025

@OrbisK @ilyaliao

changed the case from T extends any[] ? (...param: T) => void to [T] extends [any[]] ? (...param: T) => void

this keeps the backward capability also does not require [] for union types

Copy link
Collaborator

@OrbisK OrbisK left a comment

Choose a reason for hiding this comment

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

@43081j
Copy link
Collaborator

43081j commented Feb 4, 2025

i wonder if we should simplify slightly and reduce our use of any

something like this playground

type Callback<T> = IsAny<T> extends true
  ? (...param: unknown[]) => void
  : (
      [T] extends [void]
        ? () => void
        : T extends unknown[]
            ? (...param: T) => void
            : (param: T) => void
    )

its a weird type at the minute in main and in this PR because:

  • Callback<void> is (...params: unknown[]) => void which seems odd
    • who is using this and why?
    • trigger is nonsense in main, because the first param is then void (i.e. (param0: void) => void)
  • Callback<any> has ...params: any but should really at least be unknown[] IMO
    • though it could cause confusion since the type param is still any. people just shouldn't be passing this really
  • Callback<string> results in (param0: string, ...params: unknown[])
    • is this because people couldn't specify multiple params before?
    • seems like this should be (param0: string) without the made up rest params, and the caller is responsible for defining any extra params

you can merge this PR as it currently stands, don't count me as a blocker. but you should probably at least change [T] extends [unknown[]] to T extends unknown[].

does anyone know why we test against [void] and [unknown[]]? what usage fails if we change it to void and unknown[]?

@isimehmeti
Copy link
Contributor Author

isimehmeti commented Feb 4, 2025

i wonder if we should simplify slightly and reduce our use of any

something like this playground

type Callback<T> = IsAny<T> extends true
  ? (...param: unknown[]) => void
  : (
      [T] extends [void]
        ? () => void
        : T extends unknown[]
            ? (...param: T) => void
            : (param: T) => void
    )

its a weird type at the minute in main and in this PR because:

  • Callback<void> is (...params: unknown[]) => void which seems odd

    • who is using this and why?
    • trigger is nonsense in main, because the first param is then void (i.e. (param0: void) => void)
  • Callback<any> has ...params: any but should really at least be unknown[] IMO

    • though it could cause confusion since the type param is still any. people just shouldn't be passing this really
  • Callback<string> results in (param0: string, ...params: unknown[])

    • is this because people couldn't specify multiple params before?
    • seems like this should be (param0: string) without the made up rest params, and the caller is responsible for defining any extra params

you can merge this PR as it currently stands, don't count me as a blocker. but you should probably at least change [T] extends [unknown[]] to T extends unknown[].

does anyone know why we test against [void] and [unknown[]]? what usage fails if we change it to void and unknown[]?

For me, I would go with the simplest and cleanest solution:
type Callback<T> = [T] extends [any[]] ? (...params: T) => void : (param: T) => void;

with this one typing arguments is easy and clean you get what you expect and you are not limited

example: TsPlayground

@43081j
Copy link
Collaborator

43081j commented Feb 4, 2025

yes but the conditional types are there to allow Callback<any> and Callback<void>

it'd be a breaking change to the types if we drop those

Callback<any>;
// actually represents (...params: any[]) => void
// DOES NOT represent (param: any) => void

Callback<void>;
// actually represents (...params: unknown[]) => void
// DOES NOT represent (param: void) => void

for whatever reason, we have these two special cases. and that's why the type looks so wonky

@isimehmeti
Copy link
Contributor Author

isimehmeti commented Feb 4, 2025

that's why I added the change as an extra condition not to have breaking changes

but if I change [T] extends [any[]] to T extends unknown[]. It will not infer with union types like this test case on

packages/shared/createEventHook/index.test.ts

  it('should pass union type', () => {
    let count = 0

    const { on: onResult, trigger } = createEventHook<number | string>()

    // union type should be inferred
    onResult(value => count = 2)
    trigger(1)
    trigger(2)

    expect(count).toBe(2)
  })

@hndregjoni
Copy link

hndregjoni commented Feb 4, 2025

i wonder if we should simplify slightly and reduce our use of any

something like this playground

type Callback<T> = IsAny<T> extends true
  ? (...param: unknown[]) => void
  : (
      [T] extends [void]
        ? () => void
        : T extends unknown[]
            ? (...param: T) => void
            : (param: T) => void(...param: T) => void
    )

its a weird type at the minute in main and in this PR because:

  • Callback<void> is (...params: unknown[]) => void which seems odd

    • who is using this and why?
    • trigger is nonsense in main, because the first param is then void (i.e. (param0: void) => void)
  • Callback<any> has ...params: any but should really at least be unknown[] IMO

    • though it could cause confusion since the type param is still any. people just shouldn't be passing this really
  • Callback<string> results in (param0: string, ...params: unknown[])

    • is this because people couldn't specify multiple params before?
    • seems like this should be (param0: string) without the made up rest params, and the caller is responsible for defining any extra params

you can merge this PR as it currently stands, don't count me as a blocker. but you should probably at least change [T] extends [unknown[]] to T extends unknown[].

does anyone know why we test against [void] and [unknown[]]? what usage fails if we change it to void and unknown[]?

That is a trick to tackle the way the conditional type expressions work. T extends void ? M : N would, when faced with Callback<never> resolve to never, regardless of M or N. When we check against [void], Callback<never> works a bit more intuitively, and it resolves to (...param: unknown[]) => void. Similarly, for union types (never is just a case of this, since it is a union of no types), the conditional distributes over the unioned types. So, Callback<string | number> resolves to ((param_0: string, ...param: unknown[]) => void) | (param_0: number, ...param: unknown[]) => void) which, I think, is not as intuitive (and neither intended) compared to (param_0: number | string, ...param: unknown[]) => void.

Other than that, I do agree that adding the additional ...param: unknown[] seems a bit superfluous.

@43081j
Copy link
Collaborator

43081j commented Feb 4, 2025

but if I change [T] extends [any[]] to T extends unknown[]. It will not infer with union types like this test case on

aha yeah, ok makes sense!

otherwise it just applies to each member of the union, so would be something like (p0: string) => void | (p0: number) => void

Callback works a bit more intuitively, and it resolves to (param: unknown) => void

to be honest i'd love to see these three weird cases vanish in the next major version

Callback<never>; // (...params: unknown[]) => void
Callback<any>; // (...params: any) => void
Callback<void>; // (..params: unknown[]) => void

i actually don't think it is intuitive that any of these resolve to (param: unknown) => void. i think they're all very strange and are the sole reason Callback isn't a much simpler type

i would've expected this:

Callback<never>; // (param: never) => void
Callback<any>; // (param: any) => void
Callback<void>; // (param: void) => void

then i'd ask the consumer what they're trying to do, as the never and void examples here seem like nonsense

the void one seems like a hack to produce Callback<[]> (i.e. no args - but it actually produces one with any args!), and never, who knows.

i've taken this on a bit of a tangent so maybe we should discuss outside of this PR. but i do feel like this type is a bit of a mess right now

@OrbisK
Copy link
Collaborator

OrbisK commented Feb 4, 2025

but i do feel like this type is a bit of a mess right now

maybe we can fine tune this, to solve the problem with v12 and do a follow up with the breaking changes for v13?

@43081j
Copy link
Collaborator

43081j commented Feb 4, 2025

i think so

i think this PR is all good and we can discuss other changes for the next major

@ilyaliao ilyaliao added this pull request to the merge queue Feb 5, 2025
Merged via the queue into vueuse:main with commit 636b866 Feb 5, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lgtm This PR has been approved by a maintainer size:XS This PR changes 0-9 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants