Search code examples
typescriptvuejs3vue-composition-apivue-script-setup

Merging interfaces and passing the result as a Prop to defineProps in Vue 3


In vue 3 composition API im trying to do the following:

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

// vue doesnt like this line (the export seems to be the issue)
export interface ButtonItemProp extends ButtonData, Styling {}

const props = withDefaults(defineProps<ButtonItemProp>(), {
   type: "button",
   ...
});

...

But i keep getting the following error: Unexpected "}" 28 | expose(); 29 |
30 | const props = __props as }; | ^ 31 |
32 |

As soon as i add a fake property to ButtonItemProp as such:

export interface ButtonItemProp extends ButtonData, Styling {
   fake_property: boolean;
}

Then it works fine...

Interestingly I have just found that that without the export, it works fine so I have had to do this instead further down:

export type { ButtonItemProp };

Wondering if someone could explain whats going on with Vue when I am exporting it...?


Solution

  • The entire <script setup lang="ts"> will be processed by vue sfc compiler first into a typescript module, and afterwards this module is forwarded to typescript compiler for compilation. The error you saw is reported by typescript compiler, implying that vue sfc compiler generated typescript module has syntax errors.

    Long story short, the error is related to how defineProps is handled, and the minimal setup to reproduce the exact syntax error is (i.e. empty interface body):

    <script setup lang="ts">
      interface ButtonItemProp {}
      const props = withDefaults(defineProps<ButtonItemProp>(), {});
    </script>
    

    SFC compiler yields:

    import { defineComponent as _defineComponent } from 'vue'
    interface ButtonItemProp {}
    
    export default /*#__PURE__*/_defineComponent({
      setup(__props: any, { expose }) {
        expose();
        const props = __props as  };
        return { props }
      }
    })
    

    In contrast:

    <script setup lang="ts">
      interface ButtonItemProp { type: string }
      const props = withDefaults(defineProps<ButtonItemProp>(), {});
    </script>
    

    yields:

    import { defineComponent as _defineComponent } from 'vue'
    interface ButtonItemProp { type : string }
    
    export default /*#__PURE__*/_defineComponent({
      props: {
        type: { type: String, required: true }
      },
      setup(__props: any, { expose }) {
        expose();
        const props = __props as { type : string };
        return { props }
      }
    })
    
    

    Note that how const props = __props as { type : string }; always cast props to a type literal.

    This will also work:

    interface ButtonData { type: string }
    interface ButtonItemProp extends ButtonData {}
    

    But the below wouldn't and it is the bug:

    interface ButtonData { type: string }
    export interface ButtonItemProp extends ButtonData {}
    

    Basically Vue believes ButtonItemProp now has an empty body.


    Some Details

    The transformation into typescript module does not involve typescript compiler, and is done completely by vue with the help of babel-parser. Basically vue invokes babel-parser to produce an AST of the <script setup> body, and then vue walks the AST to generate the desired ts module content.

    defineProps is a special construct that participates in this stage - in other words, it is almost like a callback to be invoked by the compiler in order to generate a better transpiled result. In particular, defineProps gives the compiler hints to perform ts type inference so that the generated ts module can have the props variable properly typed. This process of inferring the props type is also done by vue (not by typescript compiler) based on the babel-parser generated AST.

    It is easy to imagine how the process roughly works:

    1. you realize defineProps is presented with a TSTypeParameterInstantiation (<ButtonItemProp>) being a TSTypeReference (ButtonItemProp)
    2. you then search the entire AST for TSIntefaceDeclaration of ButtonItemProp
    3. you realize ButtonItemProp (through its TSIntefaceDeclaration) extends ButtonData
    4. search the entire AST for TSIntefaceDeclaration of ButtonData
    5. ...
    6. after you find all the leaf level TSIntefaceDeclaration, grab their TSTypeLiteral, union them and generate the type literal for props.

    Now, the difference that export makes is that it changes the AST node type of export interface ButtonItemProp extends ...: instead of TSIntefaceDeclaration, it will emit node ExportNamedDeclaration (the actual TSIntefaceDeclaration is its child node).

    Vue compiler has a bug here that it only performs the extends resolution for TSIntefaceDeclaration, not for ExportNamedDeclaration.