offset = 0,
knownProps: PropsDestructureBindings,
error: (msg: string, node: Node, end?: number) => never,
- watchMethodName = 'watch'
+ vueImportAliases: Record<string, string>
) {
const rootScope: Scope = {}
const scopeStack: Scope[] = [rootScope]
return false
}
+ function checkUsage(node: Node, method: string, alias = method) {
+ if (isCallOf(node, alias)) {
+ const arg = unwrapTSNode(node.arguments[0])
+ if (arg.type === 'Identifier') {
+ error(
+ `"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). ` +
+ `Pass a getter () => ${arg.name} instead.`,
+ arg
+ )
+ }
+ }
+ }
+
// check root scope first
walkScope(ast, true)
;(walk as any)(ast, {
return this.skip()
}
- if (isCallOf(node, watchMethodName)) {
- const arg = unwrapTSNode(node.arguments[0])
- if (arg.type === 'Identifier') {
- error(
- `"${arg.name}" is a destructured prop and cannot be directly watched. ` +
- `Use a getter () => ${arg.name} instead.`,
- arg
- )
- }
- }
+ checkUsage(node, 'watch', vueImportAliases.watch)
+ checkUsage(node, 'toRef', vueImportAliases.toRef)
// function scopes
if (isFunctionType(node)) {
reactive,
proxyRefs,
toRef,
+ toValue,
toRefs,
ToRefs,
shallowReactive,
- readonly
+ readonly,
+ MaybeRef,
+ MaybeRefOrGetter,
+ ComputedRef,
+ computed
} from 'vue'
import { expectType, describe } from './utils'
// ref unwrapping
expectType<number>(unref(arg))
+ expectType<number>(toValue(arg))
+ expectType<number>(toValue(() => 123))
// ref inner type should be unwrapped
const nestedRef = ref({
// Should not distribute Refs over union
expectType<Ref<number | string>>(toRef(obj, 'c'))
+ expectType<Ref<number>>(toRef(() => 123))
+ expectType<Ref<number | string>>(toRef(() => obj.c))
+
+ const r = toRef(() => 123)
+ // @ts-expect-error
+ r.value = 234
+
// toRefs
expectType<{
a: Ref<number>
expectType<number>(x.value.a.b)
})
+
+describe('toRef <-> toValue', () => {
+ function foo(
+ a: MaybeRef<string>,
+ b: () => string,
+ c: MaybeRefOrGetter<string>,
+ d: ComputedRef<string>
+ ) {
+ const r = toRef(a)
+ expectType<Ref<string>>(r)
+ // writable
+ r.value = 'foo'
+
+ const rb = toRef(b)
+ expectType<Readonly<Ref<string>>>(rb)
+ // @ts-expect-error ref created from getter should be readonly
+ rb.value = 'foo'
+
+ const rc = toRef(c)
+ expectType<Readonly<Ref<string> | Ref<string>>>(rc)
+ // @ts-expect-error ref created from MaybeReadonlyRef should be readonly
+ rc.value = 'foo'
+
+ const rd = toRef(d)
+ expectType<ComputedRef<string>>(rd)
+ // @ts-expect-error ref created from computed ref should be readonly
+ rd.value = 'foo'
+
+ expectType<string>(toValue(a))
+ expectType<string>(toValue(b))
+ expectType<string>(toValue(c))
+ expectType<string>(toValue(d))
+
+ return {
+ r: toValue(r),
+ rb: toValue(rb),
+ rc: toValue(rc),
+ rd: toValue(rd)
+ }
+ }
+
+ expectType<{
+ r: string
+ rb: string
+ rc: string
+ rd: string
+ }>(
+ foo(
+ 'foo',
+ () => 'bar',
+ ref('baz'),
+ computed(() => 'hi')
+ )
+ )
+})
} from '../src/index'
import { computed } from '@vue/runtime-dom'
import { shallowRef, unref, customRef, triggerRef } from '../src/ref'
-import { isShallow, readonly, shallowReactive } from '../src/reactive'
+import {
+ isReadonly,
+ isShallow,
+ readonly,
+ shallowReactive
+} from '../src/reactive'
describe('reactivity/ref', () => {
it('should hold a value', () => {
expect(toRef(r, 'x')).toBe(r.x)
})
+ test('toRef on array', () => {
+ const a = reactive(['a', 'b'])
+ const r = toRef(a, 1)
+ expect(r.value).toBe('b')
+ r.value = 'c'
+ expect(r.value).toBe('c')
+ expect(a[1]).toBe('c')
+ })
+
test('toRef default value', () => {
const a: { x: number | undefined } = { x: undefined }
const x = toRef(a, 'x', 1)
expect(x.value).toBe(1)
})
+ test('toRef getter', () => {
+ const x = toRef(() => 1)
+ expect(x.value).toBe(1)
+ expect(isRef(x)).toBe(true)
+ expect(unref(x)).toBe(1)
+ //@ts-expect-error
+ expect(() => (x.value = 123)).toThrow()
+
+ expect(isReadonly(x)).toBe(true)
+ })
+
test('toRefs', () => {
const a = reactive({
x: 1,
triggerEffects
} from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations'
-import { isArray, hasChanged, IfAny } from '@vue/shared'
+import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
import {
isProxy,
toRaw,
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
-export function ref<T extends object>(
- value: T
-): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
+export function ref<T extends Ref>(value: T): T
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
+export type MaybeRef<T = any> = T | Ref<T>
+export type MaybeRefOrGetter<T = any> = MaybeRef<T> | (() => T)
+
/**
* Returns the inner value if the argument is a ref, otherwise return the
* argument itself. This is a sugar function for
* @param ref - Ref or plain value to be converted into the plain value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#unref}
*/
-export function unref<T>(ref: T | Ref<T>): T {
+export function unref<T>(ref: MaybeRef<T>): T {
return isRef(ref) ? (ref.value as any) : ref
}
+/**
+ * Normalizes values / refs / getters to values.
+ * This is similar to {@link unref()}, except that it also normalizes getters.
+ * If the argument is a getter, it will be invoked and its return value will
+ * be returned.
+ *
+ * @example
+ * ```js
+ * toValue(1) // 1
+ * toValue(ref(1)) // 1
+ * toValue(() => 1) // 1
+ * ```
+ *
+ * @param source - A getter, an existing ref, or a non-function value.
+ * @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue}
+ */
+export function toValue<T>(source: MaybeRefOrGetter<T>): T {
+ return isFunction(source) ? source() : unref(source)
+}
+
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
- ret[key] = toRef(object, key)
+ ret[key] = propertyToRef(object, key)
}
return ret
}
}
}
+class GetterRefImpl<T> {
+ public readonly __v_isRef = true
+ public readonly __v_isReadonly = true
+ constructor(private readonly _getter: () => T) {}
+ get value() {
+ return this._getter()
+ }
+}
+
export type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>
/**
- * Can be used to create a ref for a property on a source reactive object. The
- * created ref is synced with its source property: mutating the source property
- * will update the ref, and vice-versa.
+ * Used to normalize values / refs / getters into refs.
+ *
+ * @example
+ * ```js
+ * // returns existing refs as-is
+ * toRef(existingRef)
+ *
+ * // creates a ref that calls the getter on .value access
+ * toRef(() => props.foo)
+ *
+ * // creates normal refs from non-function values
+ * // equivalent to ref(1)
+ * toRef(1)
+ * ```
+ *
+ * Can also be used to create a ref for a property on a source reactive object.
+ * The created ref is synced with its source property: mutating the source
+ * property will update the ref, and vice-versa.
*
* @example
* ```js
* console.log(fooRef.value) // 3
* ```
*
- * @param object - The reactive object containing the desired property.
- * @param key - Name of the property in the reactive object.
+ * @param source - A getter, an existing ref, a non-function value, or a
+ * reactive object to create a property ref from.
+ * @param [key] - (optional) Name of the property in the reactive object.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#toref}
*/
+export function toRef<T>(
+ value: T
+): T extends () => infer R
+ ? Readonly<Ref<R>>
+ : T extends Ref
+ ? T
+ : Ref<UnwrapRef<T>>
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
key: K,
defaultValue: T[K]
): ToRef<Exclude<T[K], undefined>>
-export function toRef<T extends object, K extends keyof T>(
- object: T,
- key: K,
- defaultValue?: T[K]
-): ToRef<T[K]> {
- const val = object[key]
+export function toRef(
+ source: Record<string, any> | MaybeRef,
+ key?: string,
+ defaultValue?: unknown
+): Ref {
+ if (isRef(source)) {
+ return source
+ } else if (isFunction(source)) {
+ return new GetterRefImpl(source as () => unknown) as any
+ } else if (isObject(source) && arguments.length > 1) {
+ return propertyToRef(source, key!, defaultValue)
+ } else {
+ return ref(source)
+ }
+}
+
+function propertyToRef(source: object, key: string, defaultValue?: unknown) {
+ const val = (source as any)[key]
return isRef(val)
? val
- : (new ObjectRefImpl(object, key, defaultValue) as any)
+ : (new ObjectRefImpl(
+ source as Record<string, any>,
+ key,
+ defaultValue
+ ) as any)
}
// corner case when use narrows type