Subscribe on changes!

ref returned by toRefs(props) have type Ref<X | undefined> | undefined

avatar
Aug 7th 2022

Vue version

latest, 3.2.37

Link to minimal reproduction

https://codesandbox.io/s/vigorous-sound-6m9x2d

Steps to reproduce

Check sandbox link provided in minimal reproduction field and run npm run type-check in terminal: изображение

What is expected?

Expected that the type-check action returns no error in buggedComponent.vue that is the type of optionalProperty in anyProperty assignation detects as string because all conditions that may detect and prevent undefined type are passed: изображение

What is actually happening?

typescript compiler detects type of the optionalProperty in anyProperty assign condition as string|undefined

System Info

No response

Any additional comments?

If you dont want to check the reproduction link, there are some photos that describes the problem:изображение изображение изображение

avatar
Aug 7th 2022

toRef() works as expected. The prop is possibly undefined, so the generated ref possibly contains and undefined value: Ref<string | undefined>. The propblem is only with toRefs()

For toRefs(), this is a bit trickier and I'd need our TS experts like @pikax to chime in. it seems that the types of toRefs() return Ref<Type | undefined> | undefined for a possibly undefined props, which is not accurate in this scenario - the prop object will always have this key (its value just may be undefined), so we know we will always have get a Ref<string | udnefined>.

However, I think the core issue is TS rather recent differentiation between an optional property and a property whose value is undefined. The following change to the reproduction's code generates the expected type for the ref:

const props = defineProps<{
  requiredProperty: string;
  optionalProperty: string | undefined;
}>();

...and it's better reflecting reality as well: the prop can't be missing on that props object, it can only be undefined. However, this is not a proper workaround for now either, as that so-defined prop can no longer be left out when being used in a parent:

Untitled

And from this perspective, the prop should really be optional, not just its valzue possibly undefined - we want to be able to completely omit it in the parent - but the property key should exist internally on the propsobject for consistency.

Tricky 🤔

avatar
Aug 7th 2022

For toRefs(), this is a bit trickier and I'd need our TS experts like @pikax to chime in. it seems that the types of toRefs() return Ref<Type | undefined> | undefined for a possibly undefined props, which is not accurate in this scenario - the prop object will always have this key (its value just may be undefined), so we know we will always have get a Ref<string | udnefined>.

This is only valid for props, but toRefs is used in on the user land, I believe the current toRefs definition is accurate for the type coming from defineProps.

I think we can update the return type from props to remove the optional property, props can be converted to options when we create the PublicComponent type (aka $props).

avatar
Aug 7th 2022

toRef() works as expected. The prop is possibly undefined, so the generated ref possibly contains and undefined value: Ref<string | undefined>. The propblem is only with toRefs()

I agree. But, I think, the main topic of this issue is that it is impossible to separate Ref<string> from Ref<undefined>

avatar
Aug 7th 2022

But, I think, the main topic of this issue is that it is impossible to separate Ref from Ref

If you have an optional prop (= it can be undefined), then a ref created from that optional prop will possibly contain undefined. That's not a bug, and Vue can't solve that for you - you need to do that.

if you have a piece of code that expects Ref<string>, then check the ref's value before calling that code. How do do that exactly is depending on your code (for details please don't use this issue, ask the community on discord or in this repo's discussions tab). Pseudocode:

const myProp: Ref<string | undefined> = toRef(props, 'myProp')

function myFn (myProp: Ref<string>) { ... }

if (myProp.value) {
  myFn(myProp)
}

you would need to do the exact same thing for a possibly undefined plain variable (const myVar: string | undefined)

avatar
Aug 7th 2022

This is only valid for props, but toRefs is used in on the user land, I believe the current toRefs definition is accurate for the type coming from defineProps.

True, forgot to mention it, but was aware

I think we can update the return type from props to remove the optional property, props can be converted to options when we create the PublicComponent type (aka $props).

Great, agreed.

avatar
Aug 7th 2022

But, I think, the main topic of this issue is that it is impossible to separate Ref from Ref

If you have an optional prop (= it can be undefined), then a ref created from that optional prop will possibly contain undefined. That's not a bug, and Vue can't solve that for you - you need to do that.

if you have a piece of code that expects Ref<string>, then check the ref's value before calling that code. How do do that exactly is depending on your code (for details please don't use this issue, ask the community on discord or in this repo's discussions tab). Pseudocode:

const myProp: Ref<string | undefined> = toRef(props, 'myProp')

function myFn (myProp: Ref<string>) { ... }

if (myProp.value) {
  myFn(myProp)
}

you would need to do the exact same thing for a possibly undefined plain variable (const myVar: string | undefined)

Did you check the reproduction?

if(myProp.value){myFn(myProp)}  // won't work

изображение

avatar
Aug 7th 2022

Did you check the reproduction?

Your reproduction code is invalid, you are assigning ref<string> to a string.

The only valid point is the Ref<string | undefined> | undefined which is a bug.

avatar
Aug 7th 2022

Did you check the reproduction?

Your reproduction code is invalid, you are assigning ref<string> to a string.

The only valid point is the Ref<string | undefined> | undefined which is a bug.

Oh, yes, sorry. But i fixed it and anyways this returns an error: изображение изображение

avatar
Aug 7th 2022
avatar
Aug 7th 2022

Ok thank you

avatar
Aug 7th 2022

@pikax you sure? I think what OP tries to do here would require casting or a new ref.

They are checking for .value to not be nullish, but that will not make the existing Ref<string | undefined> ref into Ref<string>, as it could be set to undefined later again. Object ins TS can never change their original type.

They would need to return a fresh ref, that might work, but not the same one. This works (for both variations of the demonstrated problem:

const anyProperty: Ref<string> =
  props.optionalProperty && optionalProperty && optionalProperty.value
    ? ref(optionalProperty.value)
    : ref("qwerty");
avatar
Aug 7th 2022

@LinusBorg I believe that's typescript type narrowing, it makes sense if you think about, because the ref can be undefined, even if you check if .value is undefined nothing prevents to be changed afterwards:

declare const r : Ref<string | undefined>

const refString = r.value  ? r : ref(''); // the type you are hoping to get here is `Ref<string>` if I understand you

r === refString; // true if `r.value` is truthy.


r.value = undefined; // this is type safe because `r` allows undefined

refString.value === undefined; // because they share the same instance

As you can see from the example you must use another instance since the original ref is Ref<string|undefined>

avatar
Aug 7th 2022

Yeah thats what i meant, basically.

avatar
Aug 7th 2022

@pikax you sure? I think what OP tries to do here would require casting or a new ref.

They are checking for .value to not be nullish, but that will not make the existing Ref<string | undefined> ref into Ref<string>, as it could be set to undefined later again. Object ins TS can never change their original type.

They would need to return a fresh ref, that might work, but not the same one. This works (for both variations of the demonstrated problem:

const anyProperty: Ref<string> =
  props.optionalProperty && optionalProperty && optionalProperty.value
    ? ref(optionalProperty.value)
    : ref("qwerty");

New ref ref(optionalProperty.value) is not a solution because it breaks reactivity. Yeh this works on objects but only until object reassigned. So, if anywhere in parent element code the optionalProperty reassigned, for example:

const optionalProp: Ref<{objectProp: string;}> = ref({objectProp: "the string"})

setTimeout(()=>optionalProp.value={objectProp: "no the string"})

In child element const newProperty = ref(optionalProperty.value) the newProperty won't updated.

avatar
Aug 7th 2022

well, then create a computed property that returns the default string if the optional prop is undefined. that then will also always be a string. One way or another you have to deal with the fact that your prop can be undefined in your code.

Please use the discord community or the repo discussions to ask for further help. We want to limit this issue on this one bug we identified in the process. Thanks.

avatar
Aug 7th 2022

Okey and thank you for your time

avatar
Sep 22nd 2022

same problem。 But I found something even weirder If you dont pass in a variable to defineProps, This property will not be undefined after torefs。 The code like

const props = defineProps({
form: {
    type: Object,
    required: true,
  },
  formOptions: {
    type: Array as PropType<string[]>,
    required: true,
  },
});
const { form, formOptions } = toRefs(props);

image

This type will not be infer to undefined

avatar
Sep 22nd 2022

This type will not be infer to undefined

because you set required: true

avatar
Sep 22nd 2022

This type will not be infer to undefined

because you set required: true

but . if I code like this。the type will be undefined

const aaa = {
  form: {
    type: Object,
    required: true,
  },
  formOptions: {
    type: Array as PropType<FormOptionItem[]>,
    required: true,
  },
}

const props = defineProps(aaa);

const { form, formOptions } = toRefs(props);

image

avatar
Sep 22nd 2022
const aaa = {
  form: {
    type: Object,
    required: true,
  },
  formOptions: {
    type: Array as PropType<FormOptionItem[]>,
    required: true,
  },
} as const

without as const, TS will infer the required prop as boolean, meaning it could be true or false.

avatar
Jun 1st 2023

Actual in vue 3.3.4

avatar
Jul 10th 2023

As a workaround for now, I'm casting the return type of toRefs manually:

const { prop1, prop2 } = toRefs(props) as Required<ReturnType<typeof toRefs<PropInterface>>>;
avatar
Jul 14th 2023

As a workaround for now, I'm casting the return type of toRefs manually:

const { prop1, prop2 } = toRefs(props) as Required<ReturnType<typeof toRefs<PropInterface>>>;

image In this case, type is lost.