core icon indicating copy to clipboard operation
core copied to clipboard

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

Open Waujito opened this issue 2 years ago • 16 comments

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:изображение изображение изображение

Waujito avatar Aug 07 '22 12:08 Waujito

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 🤔

LinusBorg avatar Aug 07 '22 13:08 LinusBorg

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).

pikax avatar Aug 07 '22 14:08 pikax

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>

Waujito avatar Aug 07 '22 14:08 Waujito

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)

LinusBorg avatar Aug 07 '22 14:08 LinusBorg

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.

LinusBorg avatar Aug 07 '22 14:08 LinusBorg

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

изображение

Waujito avatar Aug 07 '22 14:08 Waujito

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.

pikax avatar Aug 07 '22 14:08 pikax

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: изображение изображение

Waujito avatar Aug 07 '22 15:08 Waujito

https://github.com/vuejs/core/pull/6421 will fix that error.

pikax avatar Aug 07 '22 15:08 pikax

Ok thank you

Waujito avatar Aug 07 '22 15:08 Waujito

@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");

LinusBorg avatar Aug 07 '22 16:08 LinusBorg

@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>

pikax avatar Aug 07 '22 17:08 pikax

Yeah thats what i meant, basically.

LinusBorg avatar Aug 07 '22 17:08 LinusBorg

@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.

Waujito avatar Aug 07 '22 18:08 Waujito

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.

LinusBorg avatar Aug 07 '22 20:08 LinusBorg

Okey and thank you for your time

Waujito avatar Aug 07 '22 20:08 Waujito

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

yangliguo7 avatar Sep 22 '22 12:09 yangliguo7

This type will not be infer to undefined

because you set required: true

LinusBorg avatar Sep 22 '22 12:09 LinusBorg

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

yangliguo7 avatar Sep 22 '22 12:09 yangliguo7

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.

LinusBorg avatar Sep 22 '22 17:09 LinusBorg

Actual in vue 3.3.4

ivanmem avatar Jun 01 '23 21:06 ivanmem

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>>>;

wlee221 avatar Jul 10 '23 23:07 wlee221

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.

ivanmem avatar Jul 14 '23 18:07 ivanmem