8000 feat(shared): ensure return types exists by OrbisK · Pull Request #4659 · vueuse/vueuse · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat(shared): ensure return types exists #4659

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

Merged
merged 4 commits into from
Apr 8, 2025

Conversation

OrbisK
Copy link
Collaborator
@OrbisK OrbisK commented Mar 14, 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 ensures consitent (naming + export) return types for "@vueuse/shared" and "fixes" some minor type issues along the way.

The following composables have still some open questions:

  • injectLocal (better type function, to support generics)
  • get
  • createInjectionState
  • isDefined? (typeguard)

Additional context

@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Mar 14, 2025
@OrbisK OrbisK changed the title feat(shared): ensure return types feat(shared): ensure return types exists Mar 14, 2025
@OrbisK OrbisK requested review from 43081j and ilyaliao March 14, 2025 13:22
import { getCurrentInstance, provide } from 'vue'
import { localProvidedStateMap } from './map'

export type ProvideLocalReturn = void
Copy link
Collaborator

Choose a reason for hiding this comment

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

do we gain anything in these cases where the return type is void?

i wonder if this is a waste until we have a non-void return type

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I thought its better for docs, because we have always a consitient return type.

@@ -1,6 +1,8 @@
import { toRefs, toValue } from 'vue'
import { reactiveComputed } from '../reactiveComputed'

export type ReactiveOmitReturn<T extends object, K extends keyof T> = Omit<T, K> | Partial<T>
Copy link
Collaborator

Choose a reason for hiding this comment

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

how come this one changed? (it used to be Omit<T, K> without the Partial)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah. you are right 😅

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually it is Omit or partial.

I have fine tuned the type like this:

type ReactiveOmitOrPartial<T, K extends keyof T | undefined = undefined> = 
  [K] extends [undefined]
    ? Partial<T>
    : Omit<T, Extract<K, keyof T>>;

type A = ReactiveOmitOrPartial<{a: string}>
type B = ReactiveOmitOrPartial<{a: string, b:number}, 'a'>

@@ -2,14 +2,16 @@ import type { MaybeRefOrGetter, Ref } from 'vue'
import { customRef, toValue } from 'vue'
import { tryOnScopeDispose } from '../tryOnScopeDispose'

export type RefAutoResetReturn<T = any> = Ref<T>
Copy link
Collaborator

Choose a reason for hiding this comment

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

i don't think we need a default as we always infer a T earlier

Suggested change
export type RefAutoResetReturn<T = any> = Ref<T>
export type RefAutoResetReturnType<T> = Ref<T>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When a user imports the type I wanted to have a good default behavior.

Copy link
Collaborator

Choose a reason for hiding this comment

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

they should always be specifying T, though. as it should be inferred from any usages

lets move this to the main thread though as i wrote more in my reply there

@43081j
Copy link
Collaborator
43081j commented Mar 16, 2025

before i continue reviewing, one change across many of these i would make is around default type params

you've changed quite a few places from T to T = any. but we don't need this as the type will be inferred, i.e. the default will never be hit

e.g. foo<T>(val: T): boolean doesn't ever need a default because T is inferred, it is whatever type val is

@OrbisK
Copy link
Collaborator Author
OrbisK commented Mar 19, 2025

before i continue reviewing, one change across many of these i would make is around default type params

you've changed quite a few places from T to T = any. but we don't need this as the type will be inferred, i.e. the default will never be hit

e.g. foo<T>(val: T): boolean doesn't ever need a default because T is inferred, it is whatever type val is

You are right that we infer this type internally, but if you use the XyzReturn outside of @vueuse/* means you will always have to pass a generic. Even if it is any or unknown. The XyzReturn<T = any> should only simplify the default case. Like the Ref type

https://github.com/vuejs/core/blob/021f8f3b690ea875e5602573caf009d796119eab/packages/reactivity/src/ref.ts#L26

Setting this only simplifies the default case.

@43081j
Copy link
Collaborator
43081j commented Mar 19, 2025

do you have an example of someone using the return type like this today?

i still wouldn't do it. i think people should be declaring the type they want rather than leaning on any even more

@OrbisK
Copy link
Collaborator Author
OrbisK commented Mar 20, 2025

do you have an example of someone using the return type like this today?

i still wouldn't do it. i think people should be declaring the type they want rather than leaning on any even more

Maybe this is just a matter of preference, but I think it is good practice to have any as the default for exported types. Lets take an imaginary example for now.

export type XyzReturn<T = any> = {
  type: Computed<'a' | 'b' | 'c'>
  data: Ref<T>
  readonly key1: string,
  readonly key2: Readonly<ShallowRef<{a: string, b:string}>>
  key3: number
}

If I have a function or whatever type that uses the XyzReturn, I can always use it, even if I do not want to use the data

// example 1: dont care about data type
function useExp1(): Pick<XyzReturn, 'key1'|'key2'> {}

Overall, I think it feels more natural to just write types like Ref instead of having to write Ref<any> (some lint rules may not even like this in userspace).

@43081j
Copy link
Collaborator
43081j commented Mar 20, 2025

All usages of it are inferred though. Nobody within vueuse would ever specify it manually because it would be inferred

By adding a default, you're implying it can sometimes be unknown/not inferred but that isn't true

It makes sense where it can't be inferred but that's a rarity in this repo as you pretty much always pass T in. Maybe in fetch or something you don't know up front but it's an edge case and those are the only ones that make sense to have a default

Your example isn't something that happens. It would be:

function useExp1<T>(input: T): XyzReturn<T>

i.e it always comes from an inferred input, making the default useless

@OrbisK
Copy link
Collaborator Author
OrbisK commented Mar 20, 2025

All usages of it are inferred though. Nobody within vueuse would ever specify it manually because it would be inferred

By adding a default, you're implying it can sometimes be unknown/not inferred but that isn't true

It makes sense where it can't be inferred but that's a rarity in this repo as you pretty much always pass T in. Maybe in fetch or something you don't know up front but it's an edge case and those are the only ones that make sense to have a default

Your example isn't something that happens. It would be:

function useExp1<T>(input: T): XyzReturn<T>

i.e it always comes from an inferred input, making the default useless

But XyzReturn is part of the public API. Internally we do infer it, and we will most likely use each type only once internally (except for overloads), but it is still a public export.

@43081j
Copy link
Collaborator
43081j commented Mar 20, 2025

It is part of the public API and we infer it publicly, from the input

There's no situation where it wouldn't be set unless someone is trying to work around the type system. In those cases they can pass typeof input or some such thing if they must. Or manually pass unknown. But it should be a rarity since all public API we expose infers T

I don't know how better to explain this. It doesn't need a default because we should always have a type, usually an inferred one.

@OrbisK
Copy link
Collaborator Author
OrbisK commented Mar 20, 2025

I don't know how better to explain this. It doesn't need a default because we should always have a type, usually an inferred one.

We cannot always infer the types in userspace. And sometimes the generic types do not even matter.

const useMyWebWorker = () => : {whatever: ShallowRef<boolean>} & Pick<UseWebWorkerReturn, 'post' | 'worker'> {
  const whatever = shallowRef(false)
  const {post, worker} = useWebWorker('/path/to.js')

  // ...

  return {
    whatever,
    post,
    worker,
  }
}

I do not even care about the data (generic type) here. Sometimes a user just wants to specify something like UseXyzReturn['whateverKey'] and does not care about the generic, which he does not even use. I can have a function that accepts

interface UseXyzReturn<T = any> {
  key1: string
  key2: boolean | ()=>boolean
  key3: T[]
}
const fn = (a: UseXyzReturn['key1'], b: UseXyzReturn['key2']) => /*  */)`

fn might not be directly related directly related to the actual data (key3) with the generic type.

Overall, I think this is just a personal preference. I am fine with dropping the T = any. But we already have this for many composables, if we decide not to use = any we could refactor the existing T = any to T.

@43081j
Copy link
Collaborator
43081j commented Mar 20, 2025

in your example, UseWebWorkerReturn wouldn't be generic as there's no type parameters on the way in

i still don't see a clear example of where the type wouldn't be inferred

in all places we consume a T, the return type may or may not also involve it:

declare function useWhatever<T>(input: T): UseWhateverReturn<T>;

there's no situation where the return type doesn't have a T already (i.e. UseWhateverReturn). the only case is someone working around the type system for some reason. but that should be rare since we would've already inferred the type from proper usage

in places like useFetch it may make sense since you would be specifying the return T but no input (the type of the response, unrelated to any inputs). in those cases it does make sense to have a sensible default

but thats an edge case. we should have a default when necessary rather than for the sake of it

antfu
antfu previously approved these changes Mar 21, 2025
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 21, 2025
@OrbisK
Copy link
Collaborator Author
OrbisK commented Mar 21, 2025

but thats an edge case. we should have a default when necessary rather than for the sake of it

Okay, now I see your point. I still kind of disagree 😅.

Also not setting defaults might create unnessesary breaking changes if we add a feature that comes with a generic to an existing composable. In this case, we will always break the existing type because we are not setting defaults.

@ilyaliao
Copy link
Member

Sorry for the late reply.

I think it's better for the docs because we always have a consistent return type.

I completely agree with this; having a unified definition is really nice :)

@antfu antfu merged commit c1d6e01 into vueuse:main Apr 8, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0