Imported refs used in `v-if` trigger "Object is possibly 'undefined'" error
Version
3.2.14
Reproduction link
Steps to reproduce
I needed to paste link to separate reproduction repository, because the SFC Playground does not seem to be performing type-checking. It's probably running in transpile-only mode.
I'm not sure whether this is problem with Vue compiler or TypeScript type checking, but there is a difference whether you assign a Ref
const inside <script lang="ts" setup>
directly or via some function's return value.
const titled1 = ref<Titled>()
const titled2 = innerSetup() as Ref<Titled | undefined>
In theory they both have the same type, but Vue compiler treats them differently when used in template like:
<h1 v-if="titled1">{{ titled1.title }}</h1>
<h1 v-if="titled2">{{ titled2.title }}</h1>
The resulting TypeScript is:
(titled1.value)
? (_openBlock(), _createElementBlock("h1", _hoisted_1, _toDisplayString(titled1.value.title), 1 /* TEXT */))
: _createCommentVNode("v-if", true),
(_unref(titled2))
? (_openBlock(), _createElementBlock("h1", _hoisted_2, _toDisplayString(_unref(titled2).title), 1 /* TEXT */))
: _createCommentVNode("v-if", true)
The first conditional is correctly understood by tsc
, but the second one yields error:
src/App.ts:38:79 - error TS2532: Object is possibly 'undefined'.
38 ? (_openBlock(), _createElementBlock("h1", _hoisted_2, _toDisplayString(_unref(titled2).title), 1 /* TEXT */))
~~~~~~~~~~~~~~~
Found 1 error.
Now, this might be a bug of TypeScript, because it should understand that if _unref(titled2)
is truthy then it is an instance of Titled
just like it does for titled1.value
. But whether it's TS bug or not it is a Vue problem. I have found no workaround for this other than using titled2?.title
in the template, but that kills the benefit of the type check.
What is expected?
We should be able to use const a = f() as Ref<Type | undefined>
in template conditions.
What is actually happening?
Compiling template fails.
I would say TypeScript is doing the right thing here since titled2.value
can possibly be undefined, and if it is indeed undefined
there would be a runtime error.
titled2
is not used through .value
. titled1.value
is working correctly. What does not work is using titled2
though _unref(titled2)
. But I would not say this is correct on TypeScript part. Both of these have the same inferred type:
titled1.value
is Titled | undefined
and _unref(titled2)
is also Titled | undefined
.
TypeScript supports conditional type inference like this:
let titledOrUndefined: Titled | undefined = getTitled()
if ( titledOrUndefined ) {
// in this block titledOrUndefined is known to be Titled
const titled = titledOrUndefined
console.log(titled.title)
}
But in Vue's generated ternary expressions it only works with .value
and not unref()
The only excuse I can find for TypeScript is that it's safer to assume myRef.value
will not change between ternary condition and its use in ternary branch. unref(myRef)
being a function could return random results. But it's just optics as myRef.value
executes a getter function.
But like I said – it might be TypeScript's issue, but our problem. One way to solve it is to make sure that we always access all refs via .value
. I don't know the details of setup function compilation. How do we know that we can access the locally created ref<>()
via .value
and why do we not know that the other variable is also a Ref
?
Oh, I didn't notice the v-if
part. I see this is strictly related to the generated render function code - technically something we should fix, although it's probably not easy. You should be able work around this by casting it though.
Casting would be an okay-ish work-around if I could cast inside setup function body to let Vue compiler know it can use .value
in the render function. But that is not picked up by Vue at this time.
So I am left with casting within the template's conditional branch and it doesn't make any sense - it kills type safety and it adds a lot of overhead. There might be 20 places where I attempt to use the object and each of them require either titled?.title
or v-model="titled as Titled"
.