language-tools icon indicating copy to clipboard operation
language-tools copied to clipboard

For generic components, having two emits with different args as "v" causes the callback to infer "unknown"

Open alex-eliot opened this issue 2 years ago • 4 comments

Consider this generic input component:

<template>
  <input
    ref="input"
    :value="modelValue"
    @input="(e) => $emit('update:modelValue', (e.target as HTMLInputElement).value)"
    @keydown="e => e.code === 'Enter' && $emit('submit')"
  />
</template>

<script setup lang="ts" generic="T">

defineProps<{
  modelValue: T,
}>()

defineEmits<{
  (e: 'update:modelValue', v: T): void
  (e: "submit", v: void): void
}>()
</script>

If you use it in a template like this:

<template>
  <div>
    <GenericComponent
      :modelValue="val ?? ''"
      @update:modelValue="v => val = v"
    />
  </div>
</template>

The v value in the @update:modelValue callback will be of type unknown.

Rewriting the (e: "submit", v: void): void as (e: "submit"): void makes the language tools infer the generic type correctly.

I understand that in this specific case the v: void is unnecessary, but I just wanted to give a heads up in case it's an easy fix, otherwise you can close this issue.

alex-eliot avatar Oct 28 '23 12:10 alex-eliot

Not sure if we should fix this issue :(

so1ve avatar Oct 30 '23 01:10 so1ve

I understand how it's an issue with typescript overloading. I'm unsure if it worked before. but feel free to close this issue. I'm content so long as people like me can have something they can find it they encounter this issue, and know how to figure out workarounds.

alex-eliot avatar Oct 30 '23 12:10 alex-eliot

Type conversion breaks with __VLS_NormalizeEmits, not sure if fix is ​​possible.

<script setup lang="ts">
const events!: __VLS_NormalizeEmits<{
    (e: "update:modelValue", v: number): void;
    (e: "submit", v: void): void;
}>;
events;
// ^? { [x: string]: (...args: unknown[]) => void; }
</script>

An alternative is to define events via defineProps.

<script setup lang="ts" generic="T">
defineProps<{
    modelValue: T,
    'onUpdate:modelValue'(v: T): void,
}>()

defineEmits<{
    (e: "submit", v: void): void
}>()
</script>

johnsoncodehk avatar Oct 30 '23 13:10 johnsoncodehk

Mainly because __VLS_ConstructorOverloads cannot handle void type as arguments.

so1ve avatar Oct 30 '23 13:10 so1ve

The problem is still existing now with the ultimate cause ↓

image

Using named tuple syntax can alleviate it.

KazariEX avatar Sep 30 '24 16:09 KazariEX

I think I found a similar issue now arising when using the emitName: [emittype] syntax.

This playground example seems to work in the playgroung, but the typecheck via vue-tsc on my machine fails with the error

error TS2322: Type '(number: bigint, isDivisibleByZero: boolean) => void' is not assignable to type '(...args: unknown[]) => any'.
  Types of parameters 'number' and 'args' are incompatible.
    Type 'unknown' is not assignable to type 'bigint'.

14   <Child v-model="someNumber" @update:modelValue="logNumberAndIsDivisibleByZero"></Child>

In the playground example I assume the update:modelValue from defineModel and defined in defineEmits:

const emit = defineEmits<{
  "update:modelValue": [number: bigint, isDivisibleByZero: boolean]
}>()

collide. The same does not happen when defining the event via:

const emit = defineEmits<{
  (e: 'update:modelValue', number: bigint, isDivisibleByZero: boolean): void
}>()

(I still don't quite understand where the (...args: unknown[]) => any is coming from)

ingoaf avatar Jan 13 '25 19:01 ingoaf

@ingoaf the type of update:modelValue event will be automatically generated, it will be intersected with the return type of defineEmits, then breaks.

KazariEX avatar Jan 13 '25 19:01 KazariEX

@KazariEX thank you!

Do you know how the generated type looks like, so the intersection ends up being (...args: unknown[]) => any ?

ingoaf avatar Jan 13 '25 20:01 ingoaf

@KazariEX thank you!

Do you know how the generated type looks like, so the intersection ends up being (...args: unknown[]) => any ?

import { defineComponent } from "vue";

type __VLS_Emit = {
    "update:modelValue": [foo: string];
};

type __VLS_ModelEmit = {
    "update:modelValue": [foo: string];
};

const Comp = defineComponent({
    __typeEmits: {} as __VLS_Emit & __VLS_ModelEmit
});

new Comp().$emit;

KazariEX avatar Jan 14 '25 04:01 KazariEX