feat(shared): ensure return types exists by OrbisK · Pull Request #4659 · vueuse/vueuse
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
OrbisK
changed the title
feat(shared): ensure return types
feat(shared): ensure return types exists
| import { getCurrentInstance, provide } from 'vue' | ||
| import { localProvidedStateMap } from './map' | ||
|
|
||
| export type ProvideLocalReturn = void |
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
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.
| import { toRefs, toValue } from 'vue' | ||
| import { reactiveComputed } from '../reactiveComputed' | ||
|
|
||
| export type ReactiveOmitReturn<T extends object, K extends keyof T> = Omit<T, K> | Partial<T> |
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)
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah. you are right 😅
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'>
| import { customRef, toValue } from 'vue' | ||
| import { tryOnScopeDispose } from '../tryOnScopeDispose' | ||
|
|
||
| export type RefAutoResetReturn<T = any> = Ref<T> |
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
| export type RefAutoResetReturn<T = any> = Ref<T> | |
| export type RefAutoResetReturnType<T> = Ref<T> |
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.
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
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
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
TtoT = any. but we don't need this as the type will be inferred, i.e. the default will never be hite.g.
foo<T>(val: T): booleandoesn't ever need a default becauseTis inferred, it is whatever typevalis
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
Setting this only simplifies the default case.
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
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
anyeven 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).
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
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.
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.
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.
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 previously approved these changes 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.
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 :)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters