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

`defineComponent` cross-property generics usage is broadened on usage

Open crutchcorn opened this issue 7 months ago • 3 comments

I have a .vue file that accepts generic types like so:

<!-- Test.vue -->
<script setup lang="ts" generic="TStr extends 'one' | 'two', TNum extends TStr extends 'one' ? 1 : 2">
const props = defineProps<{
  str: TStr,
  num: TNum,
  numFn: (num: TNum) => TNum
}>()
</script>

<template>
  <div>
    <p>str: {{ props.str }}</p>
    <p>num: {{ props.num }}</p>
  </div>
</template>

And when using it in App.vue, it works just as expected to infer the values of numFn:

<!-- App.vue -->
<script setup lang="ts">
import Test from "./Test.vue"
</script>

<template>
  <!-- val is `1` -->
  <Test str="one" :num="1" :numFn="val => val" />
  <!-- val is `2` -->
  <Test str="two" :num="2" :numFn="val => val" />
</template>

However, when using defineComponent and generics inside like so:

// Test2.ts
import { defineComponent } from "vue";

export const Test2 = defineComponent(
  <TStr extends "one" | "two", TNum extends TStr extends "one" ? 1 : 2>(props: {
    str: TStr;
    num: TNum;
    numFn: (num: TNum) => TNum;
  }) => {
    return () => props.str;
  }
);

The type inferencing no longer works:

<script setup lang="ts">
import {Test2} from './Test2'
</script>

<template>
  <!-- val is `1 | 2` -->
  <Test2 str="one" :num="1" :numFn="val => val" />
  <!-- val is `1 | 2` -->
  <Test2 str="two" :num="2" :numFn="val => val" />
</template>

Link to Reproduction

https://github.com/crutchcorn/vue-define-component-ts-broadening-bug

https://stackblitz.com/github/crutchcorn/vue-define-component-ts-broadening-bug

crutchcorn avatar Dec 06 '23 04:12 crutchcorn

Duplicate of https://github.com/vuejs/language-tools/issues/3745?

so1ve avatar Dec 06 '23 04:12 so1ve

Maybe @so1ve? I think this is different behavior (altho probably linked) as there seems to be a unionization of possible values on top of what #3745 describes, even when they shouldn't technically be possible to union over

crutchcorn avatar Dec 06 '23 04:12 crutchcorn

A hack I’ve used to fix a similar issue:

Define an explicit return type, including Volar’s special __ctx attribute.

In your case,

import { type VNode, defineComponent } from "vue";

export const Test2 = defineComponent(
  <TStr extends "one" | "two", TNum extends TStr extends "one" ? 1 : 2>(props: {
    str: TStr;
    num: TNum;
    numFn: (num: TNum) => TNum;
  }) => {
    return () => props.str;
  }
) as <TStr extends "one" | "two", TNum extends TStr extends "one" ? 1 : 2>(props: {
  str: TStr;
  num: TNum;
  numFn: (num: TNum) => TNum;
}) => VNode & {
  __ctx?: {
    props: {
      str: TStr;
      num: TNum;
      numFn: (num: TNum) => TNum;
    };
  };
};

By changing __ctx, you can add type inference for emits and slots too.

finlayacourt avatar Apr 17 '24 01:04 finlayacourt