Subscribe on changes!

$ref can only be used as the initializer of a variable declaration.

avatar
Sep 8th 2022

Vue version

3.2.*

Link to minimal reproduction

https://sfc.vuejs.org/#eNo9UFuKwzAMvIoQC21hY7O/2STQe/jHTZ02JX5gK+2H8d0rp6HGCI9GY2aU8RyCeK4GW+zSGOdAkAytYVButsFHggzRTFBgit7CgUcPysHoXSLQ0MNVk/7nzmIILox/ePqYQQhRGSinSvLdJWS49HA89UOuXT5fld5modSinHKd/DhiLwzI2LBoMowAuvvfkDPYdINSOslo684urATPxvqrWXqFzCtkqpNfNf7iJ1hjdRCP5B1H36yonUgKW9jNKeTAFSu8E4XUSpmmsS7skYSPN8kvEVdHszXCJNtcon8lE/ljhXuYguUNXah2ow==

Steps to reproduce

**import { ref } from 'vue'
 const a = data;
 let b = $ref({ ...data });
 
  const test = ()=>{
     b = $ref(a);
  }

</script>**

How should I reassign to B, if I b = $ref(a), the console says $ref can only be used as the initializer of a variable declaration.

If b = a, the message that type "T" cannot be assigned to type "RefValue<UnwrapRef>"

What is expected?

**import { ref } from 'vue'
 const a = data;
 let b = $ref({ ...data });
 
  const test = ()=>{
     b = $ref(a);
  }

</script>**

What is actually happening?

**import { ref } from 'vue'
 const a = data;
 let b = $ref({ ...data });
 
  const test = ()=>{
     b = $ref(a);
  }

</script>**

System Info

No response

Any additional comments?

No response

avatar
Sep 8th 2022

you can only use the $ref() compiler hint to declare a variable. So yes, the correct way to handle this is to do b = a. The whole point of $ref() is to be able to do b = a instead of having to write b.value = a.

The problem you seem to be having is with the types. However your reproduction does not show that. Please update your reproduction to demonstrate that problem.

It sounds as if you have a problem with a generic variable, which can happen when using refs. To give a more helpful answer I need to evaluate the actual code that creates the problem in a runnable reproduction (you might need to use stackblitz to get type evaluation, or provide a github repo.

avatar
Sep 8th 2022

export const useHooks = <T>(data: T) => {

  const initData = data;
  let data = $ref(initData );

const reset = ()=>{
  data  = initData
}

  return $$({
    data
  });
};

Cannot assign type 'T' to type 'RefValue<UnwrapRef>'

@LinusBorg Looking forward to your help.

avatar
Sep 8th 2022

This is a more general caveat with generics and refs, and not directly related to the $ref() compiler hint. We need to cover this in the docs. Hold on tight, it gets complicated.

You also used the identifier data for both the function parameter and a local variable, so here's a cleaned up version, still having the same problem:

export const useHooks = <T>(input: T) => {

  const initData = input;
  let data = $ref(initData );

const reset = ()=>{
  data  = initData
}

  return $$({
    data
  });
};

and here's the same code, using a normal ref, having the same problem:

import { ref } from 'vue'
export const useHooks = <T>(input: T) => {

  const initData = input;
  let data = ref(initData );

const reset = ()=>{
  data.value = initData
}

  return {
    data
  };
};

Here's the problem - say we call useHook with an object of the following shape:

useHook({
  name: ref('tom')
})

In this case, T would be equivalent to:

interface T {
  name: Ref<string>
}

but once we pass this to ref(), the value of that ref becomes unwrapped - the value of the ref is now of this shape:

const data = ref(input)
// type of `data.value is now equivalent to UnwrapRef<input>, which means`:
interface X {
  name: string // no more Ref<string>
}

so if you do this: data.value = input again, now you try to assign an object of the first type to a ref with the second type, and those types are not compatible because one property expects a string and the other a Ref<string>.

This all works at runtime in JS, but there is not clean way to express this in JS without casting, unfortunately. For a normal ref, you can just do this:

const data = ref(input) as Ref<T>

but it seems we do indeed have a challenge when we do our $ref compiler hint. This seems to work at first:

let data = $ref(input) as T

but this messes up the inferred return type of the $$() as it now does not. We can't just do this either:

let data = $ref(initData) as RefValue<T>

...because RefValue<> adds a marker property type to T, which is not present on the original T.

So for now, you would need to cast on each assignment, which kinda sucks:

data = input as RefValue<T>

The best strategy to not have this problem in the first place is to use tighter generic types that can't contain refs, but I realize that's not always possible or desirable.

We should try and investigate better ways to handle this.

avatar
Sep 9th 2022

I don't think it's a ref and string type problem, I am passing an object, not a REF

useHook({
 name:'tom',age:18
})

Now T type would be

{name:string;age:number}

const initData = input;
let data = $ref(initData );

data = {
   name:'tom',age:18
} 

This is a failure,"T" cannot be assigned to type "RefValue" Unable to reassign data

@LinusBorg

avatar
Sep 9th 2022

Yes, you are passing an object, just as in my example. And the problem is that this object could potentially contain a ref, because your generic argument T would allow that.