vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
46.77k stars 8.21k forks source link

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

Open henribru opened 3 years ago

henribru commented 3 years ago

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

edison1105 commented 3 years ago

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>
HcySunYang commented 3 years ago

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

LinusBorg commented 3 years ago

duplicate of #1135

henribru commented 3 years ago

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.

posva commented 3 years ago

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:

Screenshot 2021-03-26 at 11 54 28
henribru commented 3 years ago

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.

ercmage commented 3 years ago

@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
ercmage commented 3 years ago

It seems shallowReactive fixes my problem

yaquawa commented 3 years ago

I have the similar typing issue here with the setter.

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

jcppman commented 3 years ago

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,
});
KaelWD commented 2 years ago

https://github.com/microsoft/TypeScript/issues/43826

posva commented 2 years ago

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

Pentadome commented 1 year ago
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.

pikax commented 11 months ago

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.

vincerubinetti commented 6 months ago

~I'm running into what I think is the limitation being discussed here.~

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 post I've tried various things like `unref`, `toRefs`, `reactive`, `shallowReactive`, `UnwrapRef`, etc., but I can't make this both _functionally_ work and _type-wise_ work. I can either make it work, or make TypeScript think it works, not both. If someone could at least tell me whether what I have is solvable with the above, or if I will need to have a `@ts-ignore` somewhere until TS adds the mentioned functionality. Even better, if someone could point me in the right direction of the best way to solve this... I rarely use things like `reactive` and `toRefs`... I tend to just keep it simple and just use `ref`, and it usually works. ```vue ``` 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 `yarn install && yarn dev`. [vue-ts-nested-ref.zip](https://github.com/vuejs/core/files/14504077/vue-ts-nested-ref.zip)

Was able to fix my particular issue without any ts-ignores by changing the top level const selected = ref into a shallowRef, then update selected[key]s to selected[key].values as appropriate. And because it's now shallow, my "when selected changes" watch with "deep" wouldn't work, and I instead had to dynamically create watchers of each ref within the Object.entries(dropdowns.value) for loop.

Thought I had tried shallowRef as well as shallowReactive, but I guess not.