Skip to content

[Feature Request] infer v-if type narrowing on the @event #1249

@pikax

Description

@pikax

from #146

Goal

When we are writing complex templates we often have v-if that will do type narrowing on for the children, those type narrowings are really useful and is a pain to need to cast inside of the @event possibly causing bugs down the line (if a v-if is changed, etc)

// simple example
<template>
  <div v-if="test" @click="(test as number).toString()">
</template>
<script setup lang=ts>
declare const test: number | undefined;
</script>

// more complex example
<template>
  <div v-if="'a' in test" @click="(test as {a: string; aa: string}).aa.toString()"></div>
  <div v-else-if="test" @click="(test as {b: string; bb: string})..bb.toString()"></div>
</template>

<script lang="ts" setup>
let test: { a: string, aa: string } | { b: string, bb: string } | undefined;
</script>

As you can see, you need to cast it to be able to work, which is a pain, good luck inferring types correctly from third-party or nested types...

Solution

To solve this @events functions should have the same type narrowing than the render component.
This makes sense in vue because events are not fired/emitted after a component is disposed (which happens on v-if=false).

Philosophy

At first sight we can blame this to typescript, because variable can change to other types, but in vue a component is not render if the v-if condition is false, making the scope of the event pretty much the scope after the v-if (only when using prop function that might introduce a error)

Implementation

We can implement this by following this 3 rules:

  • add all the parent v-if and negate them in order if(!(BRANCH_CONDITION) return
  • If there's branches, add if(BRANCH_V_IF_CONDITION) return
  • add the @click code after the conditions

Example:

<div v-if="test" @click="test .toString()">

if(test) {
 <div onClick={$event=> {
   if(!(test)) return 
   test.toString() // safely a number here
 })
}
  <div v-if="'a' in test" @click="(test as {a: string; aa: string}).aa.toString()"></div>
  <div v-else-if="test" @click="(test as {b: string; bb: string})..bb.toString()"></div>

if (test && 'a' in test) {
    h('div', {
        onclick() {
            if (!(test && 'a' in test)) return
            test.aa.toString()
        }
    })
} else if (test) {
    h('div', {
        onClick() {
            // DO NOT NEGATE previous branches
            if ((test && 'a' in test)) return
            // NEGATE Current Branch
            if (!(test)) return;

            test.bb.toString()
        }
    })
}

playground

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions