Types don't allow refs to be assigned to reactive object properties
Version
3.0.7
Reproduction link
https://codesandbox.io/s/zealous-ptolemy-oierp?file=/src/index.ts
Steps to reproduce
- Create a reactive object, e.g. const foo =
reactive({bar: 3)}
- Assign a ref to one of its properties, e.g.
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
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.
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 computed
, although I guess that doesn't necessarily matter) while I'm talking about types? Like I mentioned what I'm doing works fine at runtime, it's strictly Typescript that forbids me from doing it. I might of course just be misunderstanding the other issue though.
Isn't this a limitation of the typing system:
- When reading from reactive you always get a raw value (no ref wrapper):
const data = reactive({ a: ref(0) }) data.a // 0
- When writing/setting, you need to use the same type, otherwise you would have to add the necessary checks when reading too
which is really inconvenient// for this to work data.a = ref(2) // you need to do this unref(data.a) // number // because data.a // Ref<number> | number
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 Can vue3 make auto unwrapping ref is optional via params in the future?
It's a source of confusion, personally i prefer no auto unwrap even it's ugly (have to write .value everytime), at least it's clear its a ref, and i don't have to fight/double think when assign/use it (lowering dev experience).
example of the problem:
interface Person {
name: string
drinkPower: Ref<number>
}
interface State {
searchText: string
person: Person | null
}
const state = reactive<State>({
searchText: '',
person: null,
})
const cindy: Person = {
name: 'Cindy',
drinkPower: ref(10),
}
// Typescript complaint it's different structure.
state.person = cindy
// Have to wrap inside dummy ref to make it work...
state.person = ref(cindy).value
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 @ts-ignore
.
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: https://github.com/microsoft/TypeScript/issues/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
https://github.com/microsoft/TypeScript/issues/56158
Once they allow it, we can support it in Vue.