Subscribe on changes!

Imported refs used in `v-if` trigger "Object is possibly 'undefined'" error

avatar
amw
Sep 23rd 2021

Version

3.2.14

Reproduction link

github.com/amw/vue-repro

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.

avatar
Sep 23rd 2021

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.

avatar
amw
Sep 23rd 2021

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

avatar
amw
Sep 24th 2021

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?

avatar
Sep 24th 2021

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.

avatar
amw
Sep 24th 2021

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