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
Putting a ref inside a ref should unwrap it, but its possible to trick the build system so it still thinks its a ref. #10799
Comments
Similar example as whats in the reproduction link: <template>Example</template>
<script setup lang="ts">
import { type Ref, ref } from 'vue';
interface DefAsInterface {
field: Ref<string>;
}
const initalizedAsEmpty = ref<DefAsInterface | undefined>();
initalizedAsEmpty.value = {
field: ref('text'),
};
initalizedAsEmpty!.value.field = "changed text"; // This gives build error, but its actually valid code
initalizedAsEmpty.value.field.value = "changed text"; // This assigns to `value` which didnt exist until this line runs
const initalizedWithValue = ref<DefAsInterface | undefined>({
field: ref('text'),
});
initalizedWithValue.value.field = "changed text"; // This gives no error, which is the correct behaviour
initalizedWithValue.value.field.value = "changed text"; // This correctly gives a build error
</script> |
I've run into this issue as well and, having dug into the reactivity types, believe that this is ultimately a conflict between the typing of Given the section of // reactivity.d.ts
export interface Ref<T = any> {
value: T;
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
*/
[RefSymbol]: true;
}
export declare function ref<T>(value: T): Ref<UnwrapRef<T>>;
export declare function ref<T = any>(): Ref<T | undefined>; // ref.ts
class RefImpl<T> {
// ...
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
// ...
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
} Using a "standard" ref:
Case 1: Typing your ref's like the docs suggest So: the value of a ref will always have had interface MyInterface {
foo: Ref<string>;
}
const myVariable = {
foo: ref<string>('a'),
} satisfies MyInterface;
const myVariableRef: Ref<MyInterface> = ref(myVariable);
// ^ This will report Ref<MyInterface> where it is actually Ref<UnwrapRef<MyInterface>>
if (myVariableRef.value.foo.value) {}
// ^ This is what your editor will tell you to do even though the last .value will return undefined due to the `get` in BaseReactiveHandler This case is a little contrived in this example because you should probably just let the type system work out the type of Case 2: Reassigning a ref // Considerring this:
// export declare function ref<T = any>(): Ref<T | undefined>;
interface MyInterface {
foo: Ref<string>;
}
const myVariable = {
foo: ref<string>('a'),
} satisfies MyInterface;
const myRef = ref<MyInterface>();
console.log(typeof myRef.value === 'undefined'); // this should log true, correctly
myRef.value = myVariable; // This is going to call `toReactive()` on `myVariable`
if (myRef.value.foo.value) {}
// Again, because the type of `myRef` is `Ref<MyInterface>` and not `Ref<UnwrapRef<MyInterface>>` your editor will think that there should be an extra `.value` on the end of this chain The consequence of this issue is that the above code needs to be changed to something like the following to actually work at runtime and compile correctly at design time: interface MyInterface {
foo: Ref<string>;
}
const myVariable = {
foo: ref<string>('a'),
} satisfies MyInterface;
const myRef = ref<UnwrapRef<MyInterface>>();
console.log(typeof myRef.value === 'undefined'); // this should log true, correctly
// One of the below options needs to be done to get the typing correct, even though none of them are required for runtime correctness -- this is a problem IMO
myRef.value = toValue(toRef(myVariable));
myRef.value = ref(myVariable).value; // This is effectively the same as above, but the `.value` on the end can get lost if the line is long
myRef.value = reactive(myVariable); // This is probably the most harmless, and is hopefully a noop considering that this is already a reactive object
myRef.value = myVariable as unknown as UnwrapRef<MyInterface>; // easily the worst option as a pattern, in my opinion
if (myRef.value.foo) {}
// The typing on this is now correct All of this is a result of running down a bug in my work-app (so I can't share the actual code where I found this problem), but please let me know if there are any questions: I think that I have researched the problem pretty extensively. |
Vue version
3.4.19
Link to minimal reproduction
https://play.vuejs.org/#eNqNVE1z0zAQ/Ss7ujiZ8TgzwClNywSaQzgA0xa4+FBjrx0VWfJI6zQh5L+zkvNNSJtTtLvv7Vs9rVdi3DTJvEUxFCPCulEZ4c1kkfE/HA12kVSPXG5lQ+CQ2gZUpqvrVJBLBedk3RhLsALp7rCENZTW1BAxbXR1kKVlg8AFMdh/q7hOE9oyyxFusRy76e64SjXwr5SoiqEnGDmyUlc3DFt7ZG60I5BaUqbkbyzGblI3tIRr32h0wvYHWl1gKTUWN72+13eKS+aZapHRx42ZqxcRLijqx9w4aO4658ZazIkR/+F6nwQOhnQAmkk3dd+2Ol4EdocrGAzggaFQGHSgDbsxM8+Q6SWgtcYyC1MjTG8nCdxLzdP642NAP+5RuJCOYp+z6ClaVcBPZJ4NjY8n2/GMwkSZqrcZ0sOOxPeDLH8v8f5mj6FReBdDiOLuhfQuT9vvKDegMlMOz7n8Q9Ls+8apl3y+6KR/BCdWvjmyZNfpvJt8aa8oP/WwknO2Y3fne+t41wbdsvFqiZiXjNuUskqenNG8p2GWVOSmbqRC+6UhyTJSMdy+11RkSpnnTyFGtkUesovnM8x/nYk/uYWPpeKrRYd2jqnY5SizFVKXntx/5ls7SNamaBVXX0jeIb+D1mvsyj6wLSz7oC6onYavBG/1g5ssCLXbDuWF+sp1qE8Ffy8+Xhh9L/dt8i7g2GGx/gswDbUq
Steps to reproduce
Look at the reproduction link.
If we define an interface with a Ref inside and then ref an instance of that interface its unclear wether or not the inner value has been unreffed or not.
At runtime it gets unreffed, but as far as the build system is concerned the inner value is still a Ref.
This only happens if you dont initalize the value. If the value is initalized when you define the ref everything works as expected.
What is expected?
The type of
field
should match what it is at runtime no matter when I assign the value.What is actually happening?
The type of
field
at design time isRef<string>
while at runtime its actually astring
.This only happens if the wrapping variable is assigned after it has been defined.
System Info
Any additional comments?
This might not be a problem with Vue itself, but rather with Vite or something else in the tooling, but I'm not sure where the correct place to report the bug is. Feel free to move it or point me in the right direction if this is the wrong place for it.
The text was updated successfully, but these errors were encountered: