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(types): UnwrapRef should unwrap refs in collections when reading objects #4899

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/reactivity/src/index.ts
Expand Up @@ -13,6 +13,9 @@ export {
ToRefs,
UnwrapRef,
ShallowRef,
UnwrappedMap,
UnwrappedWeakMap,
UnwrappedSet,
ShallowUnwrapRef,
RefUnwrapBailTypes,
CustomRefFactory
Expand Down
34 changes: 30 additions & 4 deletions packages/reactivity/src/ref.ts
Expand Up @@ -66,9 +66,6 @@ export function isRef(r: any): r is Ref {
return Boolean(r && r.__v_isRef === true)
}

export function ref<T extends object>(
value: T
): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see the use case for this overload. Am I missing something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this was added in #4734.
Should be save to remove as the added test still passes.

export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
Expand Down Expand Up @@ -287,13 +284,42 @@ export type UnwrapRef<T> = T extends ShallowRef<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>

export interface UnwrappedMap<K, V>
extends Omit<Map<K, UnwrapRefSimple<V>>, 'set'> {
set(key: K, value: V): this
}

export interface UnwrappedWeakMap<K extends object, V>
extends Omit<WeakMap<K, UnwrapRefSimple<V>>, 'set'> {
set(key: K, value: V): this
}

export interface UnwrappedSet<T>
extends Omit<Set<UnwrapRefSimple<T>>, 'add' | 'has' | 'delete'> {
add(value: T): this
has(value: T): boolean
delete(value: T): boolean
}

type UnwrapRefCollectionTypes<T extends CollectionTypes> = T extends Map<
infer TMapKey,
infer TMap
>
? UnwrappedMap<TMapKey, TMap>
: T extends WeakMap<infer TWeakMapKey, infer TWeakMap>
? UnwrappedWeakMap<TWeakMapKey, TWeakMap>
: T extends Set<infer TSet>
? UnwrappedSet<TSet>
: T

export type UnwrapRefSimple<T> = T extends
| Function
| CollectionTypes
| BaseTypes
| Ref
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
? T
: T extends CollectionTypes
? UnwrapRefCollectionTypes<T>
: T extends Array<any>
? { [K in keyof T]: UnwrapRefSimple<T[K]> }
: T extends object & { [ShallowReactiveMarker]?: never }
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime-core/src/index.ts
Expand Up @@ -153,6 +153,9 @@ export {
ToRefs,
UnwrapRef,
ShallowRef,
UnwrappedMap,
UnwrappedWeakMap,
UnwrappedSet,
ShallowUnwrapRef,
CustomRefFactory,
ReactiveFlags,
Expand Down
95 changes: 95 additions & 0 deletions test-dts/ref.test-d.ts
Expand Up @@ -70,6 +70,101 @@ function plainType(arg: number | Ref<number>) {
// should still unwrap in objects nested in arrays
const arr2 = ref([{ a: ref(1) }]).value
expectType<number>(arr2[0].a)

// unwrapped collections should all extend the native ones
// and be assignable if no unwrapping has happened
{
const someSet: Set<Ref<number>> = new Set()
const someWeakSet: WeakSet<Ref<number>> = new WeakSet()
const someMap: Map<string, Ref<number>> = new Map()
const someWeakMap: WeakMap<object, Ref<number>> = new WeakMap()
const setRef = ref(someSet)
const weakSetRef = ref(someWeakSet)
const mapRef = ref(someMap)
const weakMapRef = ref(someWeakMap)

expectType<Set<Ref<number>>>(setRef.value)
expectType<WeakSet<Ref<number>>>(weakSetRef.value)
expectType<Map<string, Ref<number>>>(mapRef.value)
expectType<WeakMap<object, Ref<number>>>(weakMapRef.value)
}

// should still unwrap in objects nested in collections
{
let someSet: Set<{ a: Ref<number> }> = new Set()
const setRef = ref(someSet)

const obj = {
a: ref(1)
}

// do not require unwrapped object
// on the `add`, `has`, and `delete` method
setRef.value.add(obj)
setRef.value.has(obj)
setRef.value.delete(obj)

// make sure to unwrap the object when reading
for (const { a } of setRef.value.values()) {
expectType<number>(a)
}

// @ts-expect-error should not be assignable back
// because `a` was unwrapped
someSet = setRef.value
}

{
let someWeakSet: WeakSet<{ a: Ref<number> }> = new WeakSet()
const weakSetRef = ref(someWeakSet)

const obj = {
a: ref(1)
}

weakSetRef.value.add(obj)
weakSetRef.value.has(obj)
weakSetRef.value.delete(obj)

// should be assignable back because WeakSets are not unwrapped
// (their values can never be read and only be used for lookup)
someWeakSet = weakSetRef.value
}

{
let someMap: Map<string, { a: Ref<number> }> = new Map()
const mapRef = ref(someMap)

const obj = {
a: ref(1)
}

// do not require unwrapped object on the `set` method
mapRef.value.set('key', obj)

// make sure to unwrap the object when reading
expectType<{ a: number } | undefined>(mapRef.value.get('key'))

// @ts-expect-error
someMap = mapRef.value
}

{
let someWeakMap: WeakMap<{}, { a: Ref<number> }> = new Map()
const weakMapRef = ref(someWeakMap)

const key = {}
const obj = {
a: ref(1)
}

weakMapRef.value.set(key, obj)

expectType<{ a: number } | undefined>(weakMapRef.value.get(key))

// @ts-expect-error
someWeakMap = weakMapRef.value
}
}

plainType(1)
Expand Down
1 change: 1 addition & 0 deletions test-dts/tsconfig.build.json
Expand Up @@ -5,6 +5,7 @@
"module": "esnext",
"strict": true,
"moduleResolution": "node",
"downlevelIteration": true,
"lib": ["esnext", "dom"]
},
"include": ["./*"]
Expand Down
3 changes: 2 additions & 1 deletion test-dts/tsconfig.json
Expand Up @@ -2,7 +2,8 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"declaration": true
"declaration": true,
"downlevelIteration": true
},
"exclude": ["../packages/*/__tests__", "../packages/template-explorer"]
}