Subscribe on changes!

Nested `ref` has invalid typing

avatar
Dec 9th 2023

Vue version

3.3.11

Link to minimal reproduction

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgJQKYDMA0cCqA7AdygEMw104BfOdKCEOAcgDcBXVRgKE5gE8xUcAGIQIcALyJOcGTVEAuFBgA8CabI0AjYlEXllAZxhRgeAOYA+dTMpXK3PgNyESYAQBMJzoqX0iIVgD0gbIAegD8nMFwBgAWEKwANp6aqIpI6AqIcNq6McamZlQA3FRAA

Steps to reproduce

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgJQKYDMA0cCqA7AdygEMw104BfOdKCEOAcgDcBXVRgKE5gE8xUcAGIQIcALyJOcGTVEAuFBgA8CabI0AjYlEXllAZxhRgeAOYA+dTMpXK3PgNyESYAQBMJzoqX0iIVgD0gbIAegD8nMFwBgAWEKwANp6aqIpI6AqIcNq6McamZlQA3FRAA

Just nest some refs, it works for first level, but doesn't work on second nested ref

What is expected?

It's expected to unwrap the second nested ref as well, it works that way in runtime, therefor the typing is wrong

What is actually happening?

it expexts to write .value, which is wrong

System Info

No response

Any additional comments?

I've done some changes in code in ref.ts:

export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]: true } // remove the `?:`

export function shallowRef<T>(value: MaybeRef<T>): ShallowRef<T> // remove the `: Ref<T> | ShallowRef<T>

and wrote a test:


describe('unwraps nested refs', () => {
  const x = ref({
    a: ref({
      b: ref(0)
    })
  })
  
  expectType<number>(x.value.a.b)
})

and works fine now. It seems like UnwrapRef treats second nested ref as shallowRef, due to ShallowRefMarker not being false (it extends ShallowRef, because [ShallowRef] is undefined, that's why removing the ?: fixed it here)..

What is the expected behaviour here: the runtime, or type?

avatar
Dec 11th 2023

The runtime type

and wrote a test:

describe('unwraps nested refs', () => {
  const x = ref({
    a: ref({
      b: ref(0)
    })
  })
  
  expectType<number>(x.value.a.b)
})

That type is already correct without any changes to the current code, please confirm on playground

What is the expected behaviour here: the runtime, or type?

The runtime type is correct, on your type declaration you not unwrapping the Ref, making the type a Ref, that's intended, since by default Refs are already unwrapped by default when used correctly.

When doing type, you should add the unwrapped ref manually, see

import { Ref, UnwrapRef } from 'vue'

type Foo = {
    foo: UnwrapRef<Ref<{
        bar: UnwrapRef<Ref<string>>
    }>>
}

type Unwrapped = UnwrapRef<Foo>
//   ^?

declare const a: Unwrapped

a.foo.bar = ''
// @ts-expect-error
a.foo.bar = 1

I'll close this issue, since is not an issue.

avatar
Dec 13th 2023

It seems to work fine as you say with literal types, but type transformations don't work as fine. I guess I didn't do too much tests in Vue's source codes, but spent a lot of time trying to understand and figure out how to type the nested Refs, instead of just using generated types from 'literals':

type Item = {
  id: number;
  title: string;
  nested: UnwrapRef<Ref<Item | null>>;
};

const item = ref<Item>({
  id: 1,
  title: "foo",
  nested: ref(null),
});

Do you know how to create a ref of such type easily? It says Type 'Ref<null>' is missing the following properties from type 'Item': id, title, nested(2739). Why does it here require null to own those fields, if it's just one of the possibilities?

I'd like an example of real usage of such types. I can't find a way to work with that using actual code, the type transformations here seem to be messing it up a bit too much.

playground here I'd like the pointer to show it's not Ref, but actual nested Item | null.

avatar
Dec 19th 2023

The types are working as intended, the issue is the complexity of it and some of typescript limitations that makes it quite confusing.

The rule for the Ref is, the generic type of T in Ref<T> is already unwrapped by default when used correctly in ref({} as MyType) , meaning when we unwrap the type we check if is a Ref<infer InnerT> and then return the innerT, innerT is already implicitly UnwrapRef<InnerT>, but in you case you're still declaring the innerT as having Ref : nested: UnwrapRef<Ref<Item | null>>;, making the extract to be Item | null since Item has a nested ref, that won't be unwrapped, the correct type would be Ref<UnwrapRef<Item> | null> in your example.

I'd like an example of real usage of such types. I can't find a way to work with that using actual code, the type transformations here seem to be messing it up a bit too much.

Most of the usage should be handled automatically by the literal types when using the functions, when you start adding Type manipulation the complexity grows exponentially, without a good understanding of how it works I admit it might be confusing, maybe this would be something to be added to the docs, I'm currently working on adding advance typescript docs for the v3.5+, might be useful to add some of type manipulation examples there.

Do you know how to create a ref of such type easily? It says Type 'Ref<null>' is missing the following properties from type 'Item': id, title, nested(2739). Why does it here require null to own those fields, if it's just one of the possibilities?

That type is UnwrapRef<Ref<Item | null>>; which is not the same as Ref<null> the correct type here would be UnwrapRef<Ref<null>>, if you remove the UnwrapRef from your example it able to infer the correct type, you can also only Ref<UnwrapRef<Item> | null>, which would be the correct type here.

I'd like an example of real usage of such types. I can't find a way to work with that using actual code, the type transformations here seem to be messing it up a bit too much.

If you give me some example that you think are complicated to do, please feel free to let me know, I can help you here and will also give me some ideas to add to the docs.


If you prefer using the types like that, you can also create an helper type to patch those refs:

type PatchRef<T> = {
  [K in keyof T]: T[K] extends Ref<infer InnerT> ? Ref<UnwrapRef<InnerT>> ? T[K]
}

That is not possible to be done to the core because the type mutations like that are undesirable because there's no short circuit to prevent UnwrapRef from happening, that's a big issue for Generic Types.

playground