ref returned by toRefs(props) have type Ref<X | undefined> | undefined
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:
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:
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 props
object for consistency.
Tricky 🤔
For
toRefs()
, this is a bit trickier and I'd need our TS experts like @pikax to chime in. it seems that the types oftoRefs()
returnRef<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 aRef<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
).
toRef()
works as expected. The prop is possibly undefined, so the generated ref possibly contains andundefined
value:Ref<string | undefined>
. The propblem is only withtoRefs()
I agree. But, I think, the main topic of this issue is that it is impossible to separate Ref<string>
from Ref<undefined>
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
)
This is only valid for
props
, buttoRefs
is used in on the user land, I believe the currenttoRefs
definition is accurate for the type coming fromdefineProps
.
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 thePublicComponent
type (aka$props
).
Great, agreed.
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 containundefined
. 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
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.
Did you check the reproduction?
Your reproduction code is invalid, you are assigning
ref<string>
to astring
.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:
https://github.com/vuejs/core/pull/6421 will fix that error.
@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 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 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 intoRef<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.
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.
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);
This type will not be infer to undefined
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);
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.
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>>>;