vuejs / language-tools

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

Circular references between Vue 3 components cannot be resolved #644

Closed benruehl closed 2 years ago

benruehl commented 2 years ago

We have two components that import each other. This works fine in Vue but leads to lots of errors inside the components' templates displayed in vscode.

We use Vue 3 single-file components with setup tag. Our components look roughly like this:

MonitorChain.vue

<template>
  <MonitorChainDialog v-if="condition"/>
</template>

<script setup lang="ts">
import MonitorChainDialog from '@/components/MonitorChainDialog.vue'
// ...
</script>

MonitorChainDialog .vue

<template>
  <MonitorChain/>
</template>

<script setup lang="ts">
import MonitorChain from '@/components/MonitorChain.vue'
// ...
</script>

The displayed errors look like this:

image

Every property and imported component of both of the components is marked with "Cannot find name 'X'".

johnsoncodehk commented 2 years ago

This is expected behavior of TypeScript. You need to change your implementation if use TS. (Change to JS / GlobalComponent / merge to single component...)

螢幕截圖 2021-10-30 下午10 16 25
benruehl commented 2 years ago

Thank you for the quick response.

If it would be an issue with typescript why does the typescript compiler run successfully then? Just to remember: The app builds and runs perfectly fine. There are no runtime or compile time errors. In addition, we opened the project in Webstorm which does not have any issues with it either.

profound7 commented 2 years ago

This is only a problem in .vue and .ts files where setup returns exports. If you write your vue code in tsx where setup returns a render function, mutually recursive components work, and type checking works too.

Foo.tsx

import { defineComponent, ref } from "vue";
import Bar from "./Bar";

interface FooProps {
    a: number;
}

export default defineComponent<FooProps>({
    setup(props) {
        const x = ref(0);
        return () => <Bar a={x.value} />;
    },
});

Bar.tsx

import { defineComponent, ref } from "vue";
import Foo from "./Foo";

interface BarProps {
    a: number;
}

export default defineComponent<BarProps>({
    setup(props) {
        const x = ref(0);
        return () => <Foo a={x.value} />;
    },
});

The above code works without any type checking errors. So if you really want proper type-checking, tsx is really great. But the downside of tsx is that I can't use vue directives like v-if and v-for and other syntactic sugars.

profound7 commented 2 years ago

I found a workaround to beat the typechecker, but you cannot use <script setup lang="ts">. You have to use <script lang="ts">.

Foo.vue (and similarly with Bar.vue)

<template>
    <Bar :a="x" />
</template>

<script lang="ts">
import { defineComponent, DefineComponent, ref } from "vue";
import Bar, { BarProps } from "./Bar";

export interface FooProps {
    a: number;
}

export default defineComponent({
    setup() {
        const _Bar: any = Bar; // putting directly in return doesn't work
        const x = ref(0);
        return {
            Bar: _Bar as DefineComponent<BarProps>,
            x,
        };
    }
});
</script>

Typechecking of the Foo and Bar props work as expected.

profound7 commented 2 years ago

I found a way to get it to work with script setup.

Foo.vue

<template>
    <Bar :a="x" />
</template>

<script setup lang="ts">
import { defineAsyncComponent, DefineComponent, ref } from "vue";
import type { BarProps } from "./Bar.vue";

export interface FooProps {
    a: number;
}

const Bar = defineAsyncComponent<DefineComponent<BarProps>>(() => import("./Bar.vue") as any);
const x = ref(0);
</script>

Simply defining an async component will cause recursive type inference between the two components, and the type checker will give up. You prevent recursion by typing the module as any, and you enable type checking by letting the compiler know the type is DefineComponent<BarProps>.

Hope this helps whoever is trying to get recursive components working.

zigomir commented 2 years ago

@profound7 thanks, works perfectly :)

Since Volar as VSC extension doesn't really have docs I wonder where this could be documented 🤔

johnsoncodehk commented 2 years ago

@zigomir nice to have a pr for add it to https://github.com/johnsoncodehk/volar/tree/master/extensions/vscode-vue-language-features#note for now. :)