-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Types don't allow refs to be assigned to reactive object properties #3478
Comments
this is intened https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/reactive.ts#L73-L84 // only unwrap nested ref
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
/**
* Creates a reactive copy of the original object.
*
* The reactive conversion is "deep"—it affects all nested properties. In the
* ES2015 Proxy based implementation, the returned proxy is **not** equal to the
* original object. It is recommended to work exclusively with the reactive
* proxy and avoid relying on the original object.
*
* A reactive object also automatically unwraps refs contained in it, so you
* don't need to use `.value` when accessing and mutating their value:
*
* ```js
* const count = ref(0)
* const obj = reactive({
* count
* })
*
* obj.count++
* obj.count // -> 1
* count.value // -> 1
* ```
*/
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> |
This has already been discussed, please see this thread, we will track it there. |
duplicate of #1135 |
I'm a bit confused about why this is a duplicate. That issue seems to be talking about runtime behavior (and is also related to |
Isn't this a limitation of the typing system:
So unless you have a proposal to be able to have the read type different from the set type, I don't think this can be changed: |
Good point, I wasn't aware of this limitation. However, it looks Typescript 4.3 will allow different types for setters and getters: https://devblogs.microsoft.com/typescript/announcing-typescript-4-3-beta/ I'm guessing it might be a good while before 4.3+ is adopted well enough to use its features in Vue's types though? If you prefer I'm fine with the issue being closed for now. |
@LinusBorg It's a source of confusion, personally i prefer no auto unwrap even it's ugly (have to write .value everytime), example of the problem:
|
It seems |
I have the similar typing issue here with the setter. https://codesandbox.io/s/stupefied-wave-ti0bb?file=/src/index.ts |
having the opposite (but technically the same) issue here. There's no way to make Typescript happy currently except using the ugly My Environment: Attempt 1: interface TodoItem {
title: string;
completed: boolean;
}
interface TodoList {
todos: TodoItem[],
};
const todos: Ref<TodoItem> = [];
const state = reactive({
todos,
});
// TS will complaint about this
state.todos.push({
title: 'Item',
completed: false,
}); Attempt 2: interface TodoItem {
title: string;
completed: boolean;
}
interface TodoList {
todos: TodoItem[],
};
const todos: Ref<TodoItem> = [];
// TS will complaint about this
const state: TodoList = reactive({
todos,
});
state.todos.push({
title: 'Item',
completed: false,
}); Attempt 3: interface TodoItem {
title: string;
completed: boolean;
}
interface TodoList {
todos: TodoItem[],
};
const todos: Ref<TodoItem> = [];
// TS will complaint about this
const state: UnwrapNestedRefs<TodoList> = reactive({
todos: [],
});
state.todos.push({
title: 'Item',
completed: false,
}); |
For anybody interested in this feature, you should upvote the necessary feature in TypeScript: microsoft/TypeScript#43826 |
import type { UnwrapRef } from "vue";
/**
* This function simply returns the value typed as `T` instead of `Ref<T>` so it can be assigned to a reactive object's property of type `T`.
* In other words, the function does nothing.
* You can assign a Ref value to a reactive object and it will be automatically unwrapped.
* @example Without `asUnreffed`
* ```
* const x = reactive({someProperty: 3});
* const y = ref(2);
* x.someProperty = y; // This is fine, but sadly typescript does not understand this. "Can not assign Ref<number> to number".
* // The getter is properly typed, this property should always return number.
* // But the setter should also be able to handle Ref<number>.
* // The setter and getter can not be typed differently in Typescript as of now.
* y.value = 5;
* console.log(x.someProperty) // expected: 5.
* ```
* @example With `asUnreffed`
* ```
* const x = reactive({someProperty: 3});
* const y = ref(2);
* x.someProperty = asUnreffed(y); // We lie to typescript that asUnreffed returns number, but in actuality it just returns the argument as is (Ref<number>)
* y.value = 5;
* console.log(x.someProperty) // expected: 5.
* ```
* @see {@link https://vuejs.org/api/reactivity-core.html#reactive} to learn about the Ref unwrapping a Reactive object does.
* @see {@link https://github.com/vuejs/core/issues/3478} and {@link https://github.com/microsoft/TypeScript/issues/43826} for the github issues about this problem.
* @param value The value to return.
* @returns Unchanged `value`, but typed as `UnwrapRef<T>`.
*/
export const asUnreffed = <T>(value: T): UnwrapRef<T> => value as UnwrapRef<T>; For now, I created this helper function to get around this problem. Works well but it does add a call to a useless function unfortunately. |
It seems the feature to fix this is being scoped out Once they allow it, we can support it in Vue. |
Ugh of course I figure this out right after I post it. I'll leave it here because it might help someone stumbling on this issue. Original postI've tried various things like If someone could at least tell me whether what I have is solvable with the above, or if I will need to have a <template>
<template v-for="(options, key) in dropdowns" :key="key">
<!-- ts errors on model value and update -->
<!-- ts thinks selected[key] is Ref<string>, but it's string -->
<AppSelect
v-if="selected[key]"
:options="options"
:model-value="selected[key]"
@update:model-value="(value) => (selected[key] = value)"
/>
</template>
{{ selected }}
</template>
<script setup lang="ts">
import { onMounted, type Ref, ref, watch } from "vue";
import AppSelect from "./AppSelect.vue";
// full set of dropdowns and options
const dropdowns = ref<Record<string, string[]>>({});
onMounted(async () => {
// load from some api
await new Promise((resolve) => setTimeout(resolve, 1000));
dropdowns.value = {
Animals: ["Cat", "Dog", "Elephant"],
Shapes: ["Triangle", "Square", "Circle"],
Colors: ["Red", "Green", "Blue"],
};
});
// currently selected values for each dropdown
const selected = ref<Record<string, Ref<string>>>({});
// when available dropdowns change
watch(
dropdowns,
() => {
// select first option by default
for (const [key, value] of Object.entries(dropdowns.value))
selected.value[key] = ref(value[0]!);
// this needs to be a ref because in reality it's a composable that 2-way-syncs with url param
// more stuff, like removing dropdowns
},
{ deep: true }
);
// when selected values change
watch(
selected,
() => {
// call another api based on selected value
console.log(selected.value["Animals"]); // ts thinks this is Ref<string>, but it really prints "Cat"
console.log(selected.value["Animals"].value); // prints undefined
},
{ deep: true }
);
</script> I tried to create a CodeSandbox for this but couldn't get the type errors to show, so here's a zip of a small reproducible repo. Run Was able to fix my particular issue without any ts-ignores by changing the top level Thought I had tried |
Version
3.0.7
Reproduction link
https://codesandbox.io/s/zealous-ptolemy-oierp?file=/src/index.ts
Steps to reproduce
reactive({bar: 3)}
foo.bar = ref(5)
What is expected?
Typescript should be fine with assigning a ref to a reactive object. It works at runtime and is even shown in the documentation: https://v3.vuejs.org/guide/reactivity-fundamentals.html#access-in-reactive-objects (the minimal reproduction I've linked is literally just that example)
What is actually happening?
Typescript complains that Ref isn't compatible with number
The text was updated successfully, but these errors were encountered: