vuejs / language-tools

⚡ High-performance Vue language tooling based-on Volar.js
https://marketplace.visualstudio.com/items?itemName=Vue.volar
MIT License
5.87k stars 400 forks source link

vue-tsc failed to infer generic types of custom directives within templates #4682

Closed mon-jai closed 2 months ago

mon-jai commented 3 months ago

Vue - Official extension or vue-tsc version

Same as playground

Vue version

3.5.0-beta.1

TypeScript version

5.6.0-beta

System Info

No response

Steps to reproduce

<template>
  <!--
    vue-tsc failed to infer type of T
    Type '(newUser: User) => void' is not assignable to type '(newValue: { id: number; }) => void'.
      Types of parameters 'newUser' and 'newValue' are incompatible.ts(2322)
  -->
  <div v-example="[user, setUser]"></div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

type User = {
  id: number;
  name: string;
};

const user = ref<User>({ id: 0, name: "User" });

function setUser(newUser: User) {
  user.value = newUser;
}

const vExample = {
  mounted<T extends { id: number }>(
    _elContainingTbody: HTMLElement,
    _binding: { value: [T, (newValue: T) => void] }
  ) { },
};

// T is inferred correctly by TypeScript
vExample.mounted(
  document.createElement("div"),
  { value: [user.value, setUser] }
)
</script>

What is expected?

vue-tsc should infer the type of T correctly in the template.

What is actually happening?

Type '(newUser: User) => void' is not assignable to type '(newValue: { id: number; }) => void'.
  Types of parameters 'newUser' and 'newValue' are incompatible.ts(2322)

Link to minimal reproduction

Playground link

Any additional comments?

No response

KermanX commented 3 months ago

This is a possible improvement. I thought of some methods, but was stuck on why TypeScript can't resolve types like ((a:number)=>number) | ((a:number)=>number). (playground)

mon-jai commented 3 months ago

@KermanX In the example you provided, f is actually inferred as never. You can verify this by hovering over type F here: playground.

It seems that type annotation has no effect in this case: null! is always inferred as never in TypeScript. You’re essentially telling the compiler that "this is null, but also not null at the same time".

However, ((a: number) => number) | ((a: number) => number) works as expected. You just need to initialize the variable with something that matches the type (e.g. null! as any). (playground)

KermanX commented 3 months ago

It seems that type annotation has no effect in this case: null! is always inferred as never in TypeScript. You’re essentially telling the compiler that "this is null, but also not null at the same time".

However, ((a: number) => number) | ((a: number) => number) works as expected. You just need to initialize the variable with something that matches the type (e.g. null! as any). (playground)

Thanks a lot! Adding as any works. However, in the first playground link, hovering f shows const f: ((a: number) => number) | ((a: number) => number) instead of never, which seems a TypeScript bug.

mon-jai commented 3 months ago

However, in the first playground link, hovering f shows const f: ((a: number) => number) | ((a: number) => number) instead of never, which seems a TypeScript bug.

Reported at: https://github.com/microsoft/TypeScript/issues/59626