feat(shared): ensure return types exists by OrbisK · Pull Request #4659 · vueuse/vueuse

@OrbisK

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

@OrbisK

@OrbisK OrbisK changed the title feat(shared): ensure return types feat(shared): ensure return types exists

Mar 14, 2025

43081j

43081j

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.

43081j

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'>

43081j

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

@43081j

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

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

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

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

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

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

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

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

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

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.

@OrbisK

@OrbisK

@ilyaliao

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