Subscribe on changes!

Types don't allow refs to be assigned to reactive object properties

avatar
Mar 24th 2021

Version

3.0.7

Reproduction link

https://codesandbox.io/s/zealous-ptolemy-oierp?file=/src/index.ts

Steps to reproduce

  1. Create a reactive object, e.g. const foo = reactive({bar: 3)}
  2. 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 isn't compatible with number

avatar
Mar 25th 2021

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>
avatar
Mar 25th 2021

This has already been discussed, please see this thread, we will track it there.

avatar
Mar 25th 2021

duplicate of #1135

avatar
Mar 25th 2021

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.

avatar
Mar 26th 2021

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
    // for this to work
    data.a = ref(2)
    // you need to do this
    unref(data.a) // number
    // because
    data.a // Ref<number> | number
    
    which is really inconvenient

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:

Screenshot 2021-03-26 at 11 54 28
avatar
Apr 2nd 2021

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.

avatar
May 10th 2021

@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
avatar
May 10th 2021

It seems shallowReactive fixes my problem

avatar
May 26th 2021

I have the similar typing issue here with the setter.

https://codesandbox.io/s/stupefied-wave-ti0bb?file=/src/index.ts

avatar
Jul 11th 2021

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,
});
avatar
Oct 25th 2021

For anybody interested in this feature, you should upvote the necessary feature in TypeScript: https://github.com/microsoft/TypeScript/issues/43826

avatar
Feb 10th 2023
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.

avatar
Oct 20th 2023

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.