vuejs/core

How to import interface for defineProps

Otto-J opened this issue · 153 comments

update:
please see:
#7394

#4294 (comment)


Version

3.2.1

Reproduction link

https://github.com/Otto-J/vue3-setup-interface-errors/blob/master/src/components/HelloWorld.vue

Steps to reproduce

clone: git clone
start: yarn && yarn dev
open: master/src/components/HelloWorld.vue
modify: import interface from './types'

What is expected?

no error

What is actually happening?

[@vue/compiler-sfc] type argument passed to defineProps() must be a literal type, or a reference to an interface or literal type.


in chinese:
我想把 props的interface抽出去,但是会报错,如果interface组件里定义的就正常渲染
in english:
I want to extract the interface of props, but an error will be reported. If the interface component is defined, it will render normally

Relevant RFC section

Currently complex types and type imports from other files are not supported. It is theoretically possible to support type imports in the future.

We'll mark it as an enhancement for the future.

希望Vue团队能早日支持,现在使用vue3.2 defineProps()为Props定义单个单个类型还可以,若涉及到联合或交叉等复杂类型 defineProps就会编译错误, 这对于类型扩展是一件非常糟糕的事情

dwanl commented

我也遇到了这个坑

same problem

I have found a workaround in my case (#4758) that allows you to provide typing to your props (or its nested members) with an interface, even if the interface is imported from another file. The workaround is to rename your interface to something else.

import { Post as PostRenamed } from '@/models/forum/PostDef';

interface Props {
  Args: {Post: PostRenamed };
}
const props = withDefaults(defineProps<Props>(), {});

I noticed that you have to do this renaming workaround if the interface name appears anywhere within your <template>, even if the name doesn't necessarily correspond to your interface.

I also find a workaround, just use defineProps(ComplexType) instead of defineProps<ComplexType>. Even the ComplexType is imported from other file, it works fine.

Despite a fairly similar setup to @phasetri's issue, I'm finding that the properties of the interface I'm trying to import resolve as attrs instead of props, which breaks what I see as expected and predictable functionality.

Control example:

<script setup lang="ts">
	interface IThingie {
		serial: string,
		id: string,
		startDate: Date | null,
		endDate: Date | null
	}

	const props = withDefaults(defineProps<IThingie>(),{});
</script>

// In vue-devtools (values from route params)
props
    {
        "id": "123",
        "serial": "asdfa",
        "startDate": null,
        "endDate" null
    }

@phasetri's solution:

<script setup lang="ts">
	import { IThingie as InterfaceRenamed } from './types';

	interface Props {
		Args: { IThingie: InterfaceRenamed }
	}
	const props = withDefaults(defineProps<Props>(),{});
</script>

// In vue-devtools (values from route params)
props
    {
        "Args": "undefined"
    }
    
attrs
    endDate: null
    id: 123
    serial: "asdfa"
    startDate: null

In my team's particular case, we want to keep the interface(s) outside of the components and use them as generics. In that way, we'd be able to import them elsewhere such as services as needed. We can't seem to export them from a separate <script lang="ts"> section either when using <script setup lang="ts"> as we run into a compilation error (I'll submit an issue for that if there isn't one yet).

I guess all of this is to say a few things:

  • that the renaming trick shouldn't be called a workaround specifically for importing interfaces
  • to pose the question of whether or not exporting the interface from the Vue component itself would provide a similar effect to exporting from a types.ts file, if we could get around the compilation error caused by two <script lang="ts"> tags, one with setup`
  • and to advocate that this change be treated with a bit more urgency than that of an enhancement for the next version.

That last one comes with some ignorance surrounding the difficulties of compiling Typescript from within Vue and the core team's release cadence, so I respect any disagreement with that request. It's just that, as a user, it does feel irksome that I can't import an interface from a file using a syntactical mode that's supposed to make Typescript adoption simpler.

To follow up on @cdvillard 's commentary -- In a world where there are many packages interacting with each other, such as Jest testing and Storybook's stories, being able to maintain a reusable interface that can be imported in our .vue components, and our other .ts files, makes a massive difference in our ability to adopt.

If it's possible to see this on a roadmap, that would greatly help with enterprise adoption. I love the simplicity and elegance of Vue, and in particular the new typescript features, but this is a major sticking point for us.

I appreciate all the hard work, I really do. But this is a bigger issue than it sounds.

If you are using Vite, you can use this plugin https://github.com/wheatjs/vite-plugin-vue-type-imports as a patch until this is officially supported.

@wheatjs , I gave the plugin a try before posting, but I couldn't get it working. Let me try again and see if I can at least post an issue with a basic recreation. I'm not sure if part of the issue had to do with running the compat build at the time, but that's no longer the case. I appreciate that you've made this though!

Created this issue on the plugin repo: wheatjs/vite-plugin-vue-type-imports#4

I've gone all-in on Vue 3, TypeScript and the Composition API. However, the restriction of requiring object literals and interfaces being defined in the SFC source severely limits reuse.

I am attempting to define a common props interface in a composable module to replace the need for a mixin, extending Props interfaces as in #4989, and using mergeProps to merge an object with default values via withDefaults(). As I soon learned, that won't work. So the only solution is to either go crawling back to mixins and the Options API, or repeat literal code in dozens of files, inviting a maintenance nightmare.

Unless anyone has a 3rd option, for which I'm open to suggestions. The vite plugin mentioned above does not solve these issues for me.

This severely hinders wrapping 3rd party components via type script, as you have to copy and paste the interfaces into your SFC, greatly increasing maintenance burdens and destroying readability. Unless there is an easier way to do this?

<script setup lang="ts">
import { Datepicker, DatePickerProps } from 'vue3-date-time-picker'

defineProps<DatePickerProps>()
defineEmits(Datepicker.emit)
</script>

<template>
  <div class="inputBox">
    <datepicker
      v-bind="$props"
      class="..."
    />
  </div>
</template>

e.g. the above is impossible currently.

Is there any way we can donate for specific issues? Or maybe open an issue hunt? I'd love to put in some extra donations specifically towards this ticket.

wait

@nborko Would it be possible for you to work with actual props objects instead of TS interfaces? Or does that raise other issues in your workflow? i.e. this works fine:

// Props.ts
export const defaultProps = {
  age: {
    type: Number,
    required: true,
  },
  job: {
    type: String,
    default: "Employee",
  },
} as const;
<script setup lang="ts">
import { defaultProps } from './Props'
const props = defineProps({
  ...defaultProps,
  name: String,
  address: {
    required: true,
  }
})
</script>

Bildschirmfoto 2022-01-09 um 16 29 45

If you need an actual type representation of the props as they are required from the parent, those can be extracted from these objects with a bit of type trickery.

If you are using Vite, you can use this plugin https://github.com/wheatjs/vite-plugin-vue-type-imports as a patch until this is officially supported.

Thank you so much for making this, @wheatjs ❤️ ! In the future, I'd also like to help fix this kind of issues. Do you have any pointers on what one should learn to be able to build or improve a plugin like yours ✨?

@LinusBorg That does work but negates the benefit of type checking the props using interfaces. Since the import in question is effectively a mixin, the props can be used in several places, and the interface may contain simple string literal types or something a lot more complex. It's helped me find a bunch of usage errors from the old 2.x code base that has worked in general, but typing everything has helped me see a lot of potential issues that I would have not otherwise found by inspection.

I've been playing around with casting individual properties as Prop<>, and that does help, as does const props: SomeInterface = defineProps() . The type annotations on the interface had to change, however, since props with defaults can no longer be "undefined" in terms of typing.

Deep diving into the implementation of the compiler functions and macros has helped me resolve some of these issues, but my conclusion is that they promise much but deliver little in actual usage. I really like the idea of them and the syntactical sugar they provide, but at this time defineProps<T> and withDefaults are far too limited to be of any practical use for me.

Thanks for the response. I'm unsure about the verdict though.

Is using a props object with additional PropType<> annotation able to do what you need or is something still missing? Is that a route that gives you typings for all props in the way you need?

Yes, I was probably too harsh, as the macros probably do work well for a good 95%+ of the people using the expected pattern. I was initially pretty frustrated when my meticulously developed type interfaces didn't work, and finally stumbled on this issue. I also didn't want to support two different coding paradigms in a single project, but in the end this may save me some work since I can copy/paste my old props definitions and annotate them instead of rewriting it from scratch.

PropType<> isn't exported, but Prop<T, D> is (which includes PropType), which seems to be working so far. It doesn't provide a super fine-grained type checking on the prop definition itself (notably, when type is an array), but it does type checking everywhere it gets used, and that's a fair enough compromise for me.

PropType<> isn't exported, but Prop<T, D> is (which includes PropType),

PropType certainly is exported by 'vue', I use it daily.

PropType<> isn't exported, but Prop<T, D> is (which includes PropType),

PropType certainly is exported by 'vue', I use it daily.

Huh, so it is. I stand corrected.

I'm now also affected by this issue and trying to workaround for 2 hours now without luck 🙁

I want to extend a quasar QBtn like so

<script setup lang="ts">
import type { QBtnProps } from 'quasar';

defineProps<QBtnProps>();
</script>

<template lang="pug">
QBtn(
  v-bind="$attrs",
  color="primary",
  flat,
  icon-right="mdi-chevron-right"
)
</template>

Just want to forward all the attributes and have TypeScript completion with Volar, but I don't want to redefine every single attribute that QBtn already knows.
Any hints how I can workaround that?

How about:

<script setup lang="ts">
import { QBtn } from 'quasar';

defineProps(QBtn.props);
</script>

How about:

<script setup lang="ts">
import { QBtn } from 'quasar';

defineProps(QBtn.props);
</script>

Sadly that doesn't help at all, due to QBtn.props is of type any

Not sure right now, but I may found the solution

<script lang="ts">
import type { QBtnProps } from 'quasar';
import { defineComponent } from 'vue';

export default defineComponent<QBtnProps>({});
</script>

<template lang="pug">
QBtn(color="primary", flat, icon-right="mdi-chevron-right")
</template>

This is not setup, but at least it seems it will automatically just forward the props 👀

any update on this? it's quite a burden. Thanks!

You can kind-of work around it by exporting only the parts of the interface you need. In this case I only really care about RouterLinkProps['to'], I can add the other parts of RouterLink as I need them.

<script setup lang="ts">
import { RouterLink } from 'vue-router'
import type { RouterLinkProps } from 'vue-router'
defineProps<{
  to: RouterLinkProps['to']
}>()
</script>

<template>
  <RouterLink :to="to" custom v-slot="{ href, navigate }">
    <a
      v-bind="$attrs"
      :href="href"
      @click="navigate"
      class="inline-flex items-center justify-center p-0.5 overflow-hidden font-medium text-gray-900 rounded-lg bg-gradient-to-br from-green-400 to-blue-600 group group-hover:from-green-400 group-hover:to-blue-600 hover:text-white dark:text-white focus:ring-4 focus:ring-green-200 dark:focus:ring-green-800"
    >
      <span
        class="px-5 py-2.5 transition-all ease-in duration-75 bg-white dark:bg-gray-900 rounded-md group-hover:bg-opacity-0"
      >
        <slot />
      </span>
    </a>
  </RouterLink>
</template>

any update?

For me this problem should get all the importance for the Vue community, because it completelly defeats the purpose of using Vue at all (with Typescript, of course). The framework was rewritten with Typescript and a simple Prop cannot be typed with actual useful types (and interfaces) that everyone use!! It's insane!!!

Vue 3.0 should not be released without this feature !!!!!

We are already on Vue 3.2.29 🤣

For me this problem should get all the importance for the Vue community, because it completelly defeats the purpose of using Vue at all (with Typescript, of course). The framework was rewritten with Typescript and a simple Prop cannot be typed with actual useful types (and interfaces) that everyone use!! It's insane!!!

You either seem to misunderstand the problem or propel it way out of proportion.

First off, you can fully and properly type your props in all cases by providing a props object instead of a type interface:

defineProps({
  name: Object as PropType<User>
})

This way of using an actual props object and PropType:

  • Works in Options API as well as composition API, with or without script setup
  • Is a bit more to type, yes.
  • gives you runtime warnings when passing the wrong (basic) type (i.e. string instead of number).
  • Works with imported props objects as well:
import Props from './sharedProps.ts'

defineProps(Props)

So, if you want to use a type interface instead of a props object, please understand that this is a new, additional feature, an optimization that, for now, has a few limitation. But they might not be as big as you think:

// This works
import { User } from './types.ts'
defineProps<{
  user: User // but you will get no *runtime* warning if you were passing a string instead of an object
}>()

So you can fully type your pops with a TS interface - but you have to write it out like above, you can't import the whole interface:

// This doesn't work
import { PropfileProps } from './types.ts'
defineProps<PropfileProps>()

We understand that this last thing is a challenge i.e. for library authors, and we have the goal of removing this limitation in the future.

But achieving that requires a certain level of complexity in the compiler that needs to be evaluated and implemented carefully and thoroughly, not least because it could also have a significant performance impact during dev if our SFC compiler has to look up and construct types from maybe multiple levels of external source files.

None of this hinders you to fully type your props with a normal props object, which is the "normal" way that worked ever since Vue 3.0 got released and before defineProps() became a thing in 3.2

@LinusBorg

when we define the props via the JS way (to be able to import/export) like so:

export const mySharedProps = {
  user: {
    type: Object as PropType<User | undefined>,
    default: undefined
  }
}

is there any way to then extract a type that would look like this:

export type MySharedProps = {
  user?: User | undefined
}

Any advice on how to achieve this would be greatly appreciated! 🍻

@mesqueeb This is possible with a bit of type magic, yes.

import {
  ExtractPropTypes,
 ExtractDefaultPropTypes,
} from 'vue'
import { SetOptional } from 'type-fest'

export type ExternalProps<T extends Record<string | number | symbol, {}>> 
  = SetOptional<ExtractPropTypes<T>, keyof ExtractDefaultPropTypes<T>>

Usage:

import { ExternalProps } from './typeUtils.ts'
import { PropTypes } from 'vue'
// Example custom types
interface User {
  name: string
}
interface Post {
  title: string
}

const mySharedProps = {
  user: {
    type: Object as PropType<User>,
    default: () => ({ name: 'Tom' })
  },
  post: {
    type: Object as PropType<Post>,
    required: true,
  },
} as const // <= THIS IS NECESSARY TO DETECT REQUIRED PROPS CORRECTLY!

export type MySharedProps = ExternalProps<typeof mySharedProps>

//This is the resulting type:
type MySharedProps = {
  user?: User | undefined // has default value internally, but from parent's perspective it's optional
  post: Post // required
}

A little bit on how this works:

ExtractPropTypes gives you almost what you want, but the resulting interface has props with a default value marked as required. This is because this interface is the internal props interface - this.$props, where props with default values are guaranteed to be present.

So we need to make these optional. How?

  • ExtractDefaultPropTypes gives us an interface with only those props that have default values
  • We then use the keys of this interface with keyof ...
  • and make these keys optional on the interface provided by ExtractPropTypes

For the last step I use SetOptional from the amazing type-fest collection of useful types, but I'm sure there's a SO answer out there that explains how to make properties on an interface optional if you don't want to add another dependency.

Also, yes - I think it would make real sense to have this in the core types.

I solved it by simply passing types as a sub-object:

~/composables/useHomePage.ts

export function useHomePage() {
  // fetches content from API with useStatic
  // returns a Ref<{
  //   seo: { title: string, description: string },
  //   introHero: { title: string, text: string, ... }
  //   ...
  // }>
}

// This type returns what's in the Ref<> returned by useHomePage
export type TUseHomepage = NonNullable<
  ReturnType<typeof useHomepage>['value']
>;

~/pages/index.vue

<script lang="ts" setup>
import IntroHero from '../components/home/intro-hero.vue';
import { useHomepage } from '~/composables/useHomePage';

const homepage = useHomepage();
</script>

<template>
  <main v-if="homepage">
    <IntroHero :data="homepage.introHero" />
  </main>
</template>

~/components/home/intro-hero.vue

<script setup lang="ts">
import type { TUseHomepage } from '~/composables/homepage';

// defineProps<TUseHomepage['introHero']>(); // doesn't work, throws "defineProps is not defined"
defineProps<{ data: TUseHomepage['introHero'] }>(); // works :D
</script>

So instead of passing props with v-bind, I pass them to :data, which is quite convenient in fact because it allows me to separate context between data and other props.

As I use the apollo cli to generate types for my GraphQL requests, I then can use the generated types in my components.
I have my data props, which are strings and images returned by the API, and I have my "logic" props, like :visible, :active, and so forth.

third party usecase typescript - using volar
nested namespace and interface not working in SFC <script lang="ts"> and <script setup lang="ts">

<script setup lang="ts">
import DataTables from 'datatables.net'

interface Props {
  options?: DataTables.Settings; //error: 'DataTables' only refers to a type, but is being used as a namespace here
  options?: DataTables["Settings"]; //error: 'DataTables' only refers to a type, but is being used as a namespace here
  columns?: DataTables.ColumnSettings[]; //error: 'DataTables' only refers to a type, but is being used as a namespace here
  checkbox?: boolean;
  footer?: boolean;
  constant?: boolean;
}
</script>

these exact Props works well in external type or interface file but imports from other files are not supported


Update:
Problem Above Solved with extended jquery-extended.d.ts be sure type included in tsconfig.json

/// <reference types="jquery"/>
/// <reference types="datatables.net"/>

interface JQueryStatic {
    DataTable(opts?: DataTables.Settings): DataTables.Api;
    dataTable: DataTables.StaticFunctions;
}

setup 语法糖中的这种语法什么时候可以支持

	interface Props extends IconProps {
		value?: string;
		showAction?: boolean;
		animation?: boolean;
		actionStyle?: object;
		actionText?: string;
	}
// Icon.vue
<script lang="ts">
// summary: add comment above some line
export enum EIconSize {
  XS,
  SM,
  MD,
  LG,
  XL
}
// interface must not be imported which use for defineProps
export interface IIconProps {
  // ... if next line is custom type, add comment here, i don't know why, but it worked
  size: EIconSize
  title: string
}
</script>
<script setup lang="ts">
// ... if next line is defineProps, add comment here, i don't know why, but it worked
const props = withDefaults(defineProps<IIconProps>(), {
  size: EIconSize.MD,
  title: 'Vue'
})
</script>
// X.vue
<script setup lang="ts">
import { EIconSize, IIconProps } from './Icon.vue'
</script>

setup 语法糖中的这种语法什么时候可以支持

	interface Props extends IconProps {
		value?: string;
		showAction?: boolean;
		animation?: boolean;
		actionStyle?: object;
		actionText?: string;
	}

不加?控制台还会报错,Partial 这种 ts 方法还不能用,头疼啊

How about using keyword extends?

// sharedProps.ts
export interface SharedProps {
  foo: string;
  bar?: number;
}
<script lang="ts" setup>
import { SharedProps } from "./sharedProps.ts";

interface Props extends SharedProps {}
const props = defineProps<Props>(); // The type props.foo can be inferred to be a string
</script>

☝🏼 It didn't work. I can infer the type, but it seems to be an empty object when compiling.

How about using keyword extends?

// sharedProps.ts
export interface SharedProps {
  foo: string;
  bar?: number;
}
<script lang="ts" setup>
import { SharedProps } from "./sharedProps.ts";

interface Props extends SharedProps {}
const props = defineProps<Props>(); // The type props.foo can be inferred to be a string
</script>

☝🏼 It didn't work. I can infer the type, but it seems to be an empty object when compiling.

this is also not surpport yet.

We are still waiting for a fix?

any update on this

// Icon.vue
<script lang="ts">
// summary: add comment above some line
export enum EIconSize {
  XS,
  SM,
  MD,
  LG,
  XL
}
// interface must not be imported which use for defineProps
export interface IIconProps {
  // ... if next line is custom type, add comment here, i don't know why, but it worked
  size: EIconSize
  title: string
}
</script>
<script setup lang="ts">
// ... if next line is defineProps, add comment here, i don't know why, but it worked
const props = withDefaults(defineProps<IIconProps>(), {
  size: EIconSize.MD,
  title: 'Vue'
})
</script>
// X.vue
<script setup lang="ts">
import { EIconSize, IIconProps } from './Icon.vue'
</script>

How did you find this? It's pretty interesting. with this comment, it's work. But without this comment or change the comment content, it will compile failed. WHY???

@tony-gm I've come across something like this before -- I think the root problem wasn't the lack of a comment, but rather:

<script setup lang="ts"> does not work

<script lang="ts" setup> works

Is that the same for you?

@yousefamar I'm having similar problems as the ticket starter but changing the "setup" attribute doesn't fix it for me.
The following gives the error: "Syntax Error: TypeError: Cannot read property 'content' of null"
image

Adding the interface to the local file works:
image

Also, wrapping the interface (loaded from external file) in a property fixes the error message:
image

Screenshot of Models/FormData.ts:
image

Is wrapping the interface in a property by design or is this a bug? Or am I missing something...?

// Icon.vue
<script lang="ts">
// summary: add comment above some line
export enum EIconSize {
  XS,
  SM,
  MD,
  LG,
  XL
}
// interface must not be imported which use for defineProps
export interface IIconProps {
  // ... if next line is custom type, add comment here, i don't know why, but it worked
  size: EIconSize
  title: string
}
</script>
<script setup lang="ts">
// ... if next line is defineProps, add comment here, i don't know why, but it worked
const props = withDefaults(defineProps<IIconProps>(), {
  size: EIconSize.MD,
  title: 'Vue'
})
</script>
// X.vue
<script setup lang="ts">
import { EIconSize, IIconProps } from './Icon.vue'
</script>

How did you find this? It's pretty interesting. with this comment, it's work. But without this comment or change the comment content, it will compile failed. WHY???

when i wanna export interface/enum/type from .vue to .ts, so try lot of ways, now i give up, e.g at below is worked.

<script lang="ts">
// enum here worked
export enum E {
  // ...
}
</script>
<script setup lang="ts">
import E from x.enum
// enum here error
export enum E {
  // ...
}
export interface I {
  // ...
}
// for defineProps interface must in setup
export interface IProps {
  e: E
  i: I
  // ...
}
// instead of withDefault's default values, because they probably are not literal
const DEFAULT: Required<IProps> = {
  // ...
}
const props = defineProps<IProps>()
const computed = (() => deepMerge(DEFAULT, props))
</script>

any update?

Just in case anyone needs @LinusBorg example without type-fest lib:

import { ExtractPropTypes, ExtractDefaultPropTypes } from 'vue';

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type PartialBy<T, K> = Omit<T, K> & Partial<T>;
type Writeable<T> = { -readonly [P in keyof T]: T[P] };

export type TypeFromProps<T> = Writeable<PartialBy<ExtractPropTypes<T>, keyof ExtractDefaultPropTypes<T>>>;
export type TypeFromProps<T> = Writeable<PartialBy<ExtractPropTypes<T>, keyof ExtractDefaultPropTypes<T>>>;

How to use it inside component when defining props ?

@aislanmaia for example

import { defineComponent, PropType, h } from 'vue';
import { ExtractPropTypes, ExtractDefaultPropTypes } from 'vue';

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type PartialBy<T, K> = Omit<T, K> & Partial<T>;
type Writeable<T> = { -readonly [P in keyof T]: T[P] };

export type TypeFromProps<T> = Writeable<PartialBy<ExtractPropTypes<T>, keyof ExtractDefaultPropTypes<T>>>;

// ======

const props = {
  title: {
    type: String as PropType<string>,
    required: true,
  },
  padding: {
    type: Boolean as PropType<boolean>,
    required: false,
    default: true,
  },
  negative: {
    type: Boolean as PropType<boolean>,
    required: false,
  },
  disabled: {
    type: Boolean as PropType<boolean>,
    required: false,
  },
} as const;

export type Props = TypeFromProps<typeof props>;


export default defineComponent({
  props: props,
  setup(props) {
    console.log(props);
    return [
      h('h1'),
    ];
  }
});

any news..?

Is this issue still resolved?

+1 for needing this also. I try to wrap all components, especially third-party libraries so I need to be able to export their interfaces into the wrapped components.

Any news?

@jnarowski here is a little workaround which might work for you if you want to get Intellisense for props from an imported interface on your wrapper. Tested in VsCode with Volar.

Vue will not create props for properties on the imported interface but we still can pass them through with $attrs and then provide Intellisense through our own interface which extends the imported one.

<template>
  <LibraryComponent v-bind="$attrs" /> 
</template>

<script setup lang="ts">
...
// has to be inside the wrapper component
// vue will not know about the properties inside LibraryProps that's why we have to pass them by $attrs
interface MyProps extends LibraryProps {
    // a prop that we add on top of LibraryProps
    myProp?: string
}   

const props = defineProps<MyProps>()
...

If we want to add a default value for props from the imported interface we can add only those to our interface. Props for those will be created as usually and we can supply default values.

One downside at the moment is, that Volar will complain about the props on our interface not being defined in the template section of the wrapper but it's just an IDE issue.

<template>
  <LibraryComponent 
    v-bind="$attrs" 
    :aLibraryProp="aLibraryProp" <-- our default value
  /> 
...

<script setup lang="ts">
... 
// has to be inside the wrapper component
// vue will not know about the properties on LibraryProps that's why we have to pass them with $attrs
interface MyProps extends LibraryProps {
    // a prop that we want to give a default value
    aLibraryProp: string

    // a prop that we add on top of LibraryProps
    myProp?: string
}   

const props = withDefaults(defineProps<MyProps>(), {
   aLibraryProp: 'defaultValue'
}
...

It's a messy workaround but for wrapping library components which come with many props it's still useful.

hi, any news about this? people with vite has a temporary solution with the plugin, but what about vue-cli?

@TuringJest thank you for the explanation!

I basically did the same thing in my project with Quasar UI but had to learn it the hard way. I also use inheritAttrs: false to make sure that $attrs will be passed to the right child component and bind props to the child component as well because binding only $attrs is not enough sometimes.

For e.g.

<script setup lang="ts">
  import { QBtnProps } from 'quasar';

  // ✨ Types
  export interface LBtnProps extends QBtnProps {
    someNewProp?: string
  }
  
  // 🚧 Props
  const props = withDefaults(
    defineProps<LBtnProps>(), { someNewProp: 'someDefaultValue' }
  );

  ...
</script>

<script lang="ts">
  export default {
    inheritAttrs: false
  };
</script>

<template>
  ...
  <QBtn
    v-bind="{ ...$attrs, ...props }"
    ...
  />
  ...
</template>

Any news?

It's painful to reuse the props interface 😩 any satisfied solution?

Any news?

Note: Please stop posting messages like "any news?" If there is an update, you will see it here. No updates means no updates. Spamming the same message over and over again will not help it get resolved earlier.

Question: Is it possible to force webpack to import and then paste the type before compiling?

For example:

code

<script lang="ts" setup>
import { SharedProps } from "./sharedProps.ts";

interface Props extends SharedProps {}
const props = defineProps<Props>(); // The type props.foo can be inferred to be a string
</script>

compile before SFC (it is like webpack.DefinePlugin):

<script lang="ts" setup>
// import { SharedProps } from "./sharedProps.ts";
// auto copy and paste:
interface SharedProps {
  foo: string;
  bar?: number;
}

interface Props extends SharedProps {}
const props = defineProps<Props>(); // The type props.foo can be inferred to be a string
</script>

Maybe that will be better.
vscode plugin or vite plugin ?

<script lang="ts" setup>
import { SharedProps } from "./sharedProps.ts";

interface Props extends SharedProps {
  // auto copy and paste:
  foo: string;
  bar?: number;
}
const props = defineProps<Props>(); // The type props.foo can be inferred to be a string
</script>

My draft solution

Component:
image

Types:
image

All working:
image

Solution:

Add custom loader

        {
          test: /\.vue$/,
          use: {
            loader: path.resolve(__dirname, './WebpackLoaderVueSFCTypes.js'),
          },
        },

image

And write loader (WebpackLoaderVueSFCTypes.js):

const fs = require('fs');
const path = require('path');

const regExpDefineProps = /defineProps<(\w+)>\(\)/;

function getRegExpImportDefault(typeName) {
  return new RegExp(`import type ${typeName} from ['"](.*?)['"]`);
}

function getTypeName(source) {
  return source.match(regExpDefineProps)?.[1];
}

function getTypePath(typeName, source, vueFilePath) {
  if (!typeName) {
    return null;
  }
  // todo need to support `import { Type } from` and multilines
  const filePath = source.match(getRegExpImportDefault(typeName))?.[1];

  // todo need to support aliases like `@/`
  const fullPath = path.resolve(
    path.dirname(vueFilePath),
    filePath.endsWith('.ts') ? filePath : `${filePath}.ts`,
  );

  if (!fs.existsSync(fullPath)) {
    console.warn(new Error(`Not found file ${fullPath}`));
    return null;
  }

  return fullPath;
}

function getTypesFromFile(typeName, typePath) {
  // todo better will analysis ts file, without regexp or replace
  return fs.readFileSync(typePath)
    .toString()
    .replace(/import.*?;.*/g, '')
    .replace(/export.*/g, '');
}

function WebpackLoaderVueSFCTypes(source) {
  const typeName = getTypeName(source);
  const typePath = getTypePath(typeName, source, this.resource);
  if (!typeName || !typePath) {
    return source;
  }

  return source.replace(
    getRegExpImportDefault(typeName),
    getTypesFromFile(typeName, typePath),
  );
}

module.exports = WebpackLoaderVueSFCTypes;

Notice: I stopped developing the loader because the types in the component are comfortable.
I need the ability to export from component

Ha! I found a better solution! And Webstorm is understand it!

function WebpackLoaderVueSFCTypes(source) {
  if (!source.includes('export type')) {
    return source;
  }

  return source.replaceAll(
    'export type',
    'type',
  );
}

module.exports = WebpackLoaderVueSFCTypes;

Component:
image

Storybook (can import styles from vue component):
image

Import component to other component is supported types
image

@AndreiSoroka what if step type is a complex object instead a Number ?

@aislanmaia

hi

for SFC comiler it is not important (link)
image
image

P.s.
I provided two solutions

  1. Import types into the component
  2. Export types from component

I only use the second option. The first option needs to be improved. The first option is more of a working concept (I don't want to support it).

P.s.s. Will there be any feedback from the core team? Thanks

when will it be supported ??😂

based on above comments, I got it working by extending the imported interface in the setup script (typescript)

import { PaginationOptions } from '~~types`

interface PaginationProps extends PaginationOptions { }

defineProps<PaginationProps>() // OR with default values, use below
withDefaults(defineProps<PaginationProps>(), { [YOUR DEFAULT VALUES] })

I just observed this does not work, it only takes away the error, and the props don't get passed down using withDefaults

Because I ran into this problem* today, but with defineEmits, I am boosting the search-engine signal for defineEmits as this seems to be the overarching issue tracking a single solution for both.

*to clarify, like others I've tried to extract Type literals for use in defineProps and defineEmits from a set of related components and put it along other composables.

ckvv commented

As a workaround, the value can be used instead of the type definition

❌As follows I encapsulated an input component with a label

<script setup lang="ts">
import { ElInput } from "element-plus";
import type { InputEmits, InputProps } from "element-plus";

defineProps<
  InputProps & {
    label?: string;
  }
>();
defineEmits<InputEmits>();
</script>

<template>
  <div class="w-input">
    {{ label }}
    <el-input v-bind="$attrs">
      <template #[slotName]="slotProps" v-for="(slot, slotName) in $slots">
        <slot :name="slotName" v-bind="slotProps" />
      </template>
    </el-input>
  </div>
</template>

The following type error was thrown while referencing

Type '{}' is not assignable to type 'IntrinsicAttributes & Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ readonly type: string; readonly disabled: boolean; readonly modelValue: EpPropMergeType<(new (...args: any[]) => (string | number | null | undefined) & {}) | (() => string | ... 2 more ... | undefined) | ((new (...args...'.
  Type '{}' is missing the following properties from type 'Omit<Readonly<ExtractPropTypes<__VLS_TypePropsToRuntimeProps<{ readonly type: string; readonly disabled: boolean; readonly modelValue: EpPropMergeType<(new (...args: any[]) => (string | number | null | undefined) & {}) | (() => string | number | null | undefined) | ((new (...args: any[]) => (string | ... 2 more ... ...': type, disabled, modelValue, autosize, and 11 more.

✅Below is no error and has correct type hints

<script setup lang="ts">
import { ElInput, inputEmits, inputProps } from "element-plus";
defineProps({
  ...inputProps,
  label: String,
});
defineEmits(inputEmits);
</script>

<template>
  <div class="w-input">
    {{ label }}
    <el-input v-bind="$attrs">
      <template #[slotName]="slotProps" v-for="(slot, slotName) in $slots">
        <slot :name="slotName" v-bind="slotProps" />
      </template>
    </el-input>
  </div>
</template>

OR

<script lang="ts">
import { defineComponent } from "vue";
import { inputProps, inputEmits } from "element-plus";

export default defineComponent({
  props: {
    ...inputProps,
    label: String,
  },
  emits: inputEmits,
});
</script>

<template>
  <div class="w-input">
    {{ label }}
    <el-input v-bind="$attrs">
      <template #[slotName]="slotProps" v-for="(slot, slotName) in $slots">
        <slot :name="slotName" v-bind="slotProps" />
      </template>
    </el-input>
  </div>
</template>

If you are using Vite, you can use this plugin https://github.com/wheatjs/vite-plugin-vue-type-imports as a patch until this is officially supported.

This is great, thank you!!

When wrapping a third party component, I want to expose its props.

import { Modal } from "@arco-design/web-vue";

// does not work
// defineProps<InstanceType<typeof Modal>["$props"]>();


// works
export type ModalProps = InstanceType<typeof Modal>["$props"];
export interface Props extends ModalProps {}
defineProps<Props>();

If you are using Vite, you can use this plugin https://github.com/wheatjs/vite-plugin-vue-type-imports as a patch until this is officially supported.

That's all for now🫡

As someone coming from a strong React + Typescript team I must saying not being able to have shareable reusable types across components is quite a let down. Especially when it comes to things like being able to directly use swagger schema generated Typescript definitions for which I've done a lot of in the past. Is there anyway to help contribute to resolve this?

Just for the record, Evan You told it would be addressed in Vue 3.3.

  • The video states they would start working on it after the 2.7 stable release which happened on July 1, 2022.
  • Vue 3.3 ETA seems to be Q3 2022, let us be patient, friends 😃
lzxb commented

Do you have a release plan?

I found some hack. Currently, it works for me. Just wrap imported type with Omit.

<script setup lang="ts">
  import { Button as AButton, ButtonProps } from "ant-design-vue";
  interface Props extends Omit<ButtonProps, ""> {}

  const props = defineProps<Props>();
</script>

<template>
  <AButton v-bind="props">
    <slot />
  </AButton>
</template>

I found some hack. Currently, it works for me. Just wrap imported type with Omit.

<script setup lang="ts">
  import { Button as AButton, ButtonProps } from "ant-design-vue";
  interface Props extends Omit<ButtonProps, ""> {}

  const props = defineProps<Props>();
</script>

<template>
  <AButton v-bind="props">
    <slot />
  </AButton>
</template>

I can approve that this works 👀 dafuq
But it makes my code 1 line longer in comparison to the prev workaround + I get a eslint warning for an empty interface 🙈
I would still prefer an official solution

I found some hack. Currently, it works for me. Just wrap imported type with Omit.

<script setup lang="ts">
  import { Button as AButton, ButtonProps } from "ant-design-vue";
  interface Props extends Omit<ButtonProps, ""> {}

  const props = defineProps<Props>();
</script>

<template>
  <AButton v-bind="props">
    <slot />
  </AButton>
</template>

I can approve that this works 👀 dafuq But it makes my code 1 line longer in comparison to the prev workaround + I get a eslint warning for an empty interface 🙈 I would still prefer an official solution

Yes! I agree with you. It's more like a temporary solution...

I found some hack. Currently, it works for me. Just wrap imported type with Omit.

<script setup lang="ts">
  import { Button as AButton, ButtonProps } from "ant-design-vue";
  interface Props extends Omit<ButtonProps, ""> {}

  const props = defineProps<Props>();
</script>

<template>
  <AButton v-bind="props">
    <slot />
  </AButton>
</template>

If I use this solution and withDefaults, I get a bizarre error:

Unexpected "}"
13 |    setup(__props: any, { emit: emits }) {
14 |  
15 |  const props = __props as  }
   |                            ^
16 |  
17 |  ;
myesn commented

Many thanks to Evan You and the rest of the community for developing and maintaining Vue, but I'm sorry I have to comment because this issue has been around for a year and I don't mean to rush you to fix it, I just want to know if there's a temporary solution, thanks again for your efforts.

Many thanks to Evan You and the rest of the community for developing and maintaining Vue, but I'm sorry I have to comment because this issue has been around for a year and I don't mean to rush you to fix it, I just want to know if there's a temporary solution, thanks again for your efforts.

Have you seen this #4294 (comment) ?

myesn commented

Many thanks to Evan You and the rest of the community for developing and maintaining Vue, but I'm sorry I have to comment because this issue has been around for a year and I don't mean to rush you to fix it, I just want to know if there's a temporary solution, thanks again for your efforts.

Have you seen this #4294 (comment) ?

@soerenmartius Thank you very much for your reply! I really didn't notice this comment because there were so many comments in this issue that I didn't read them carefully, this looks like a viable solution, thank you again!😊

i tried most of these workarounds, how it's not supported yet? the vite plugin works but it's a little bit frustrating when facing this issue

I found that we can used it this way:

<script
  setup
  lang = "ts">
  import type {Foo} from 'bar'
  const props = withDefaults(defineProps<{
    foo : Foo
  }>(), {
    foo: () => {
      return {
        fooProperty1: default1
      }
    }
  })
</script>

Basically, by declaring a property on the prop which can have the required Typing.

kbbdy commented

Maybe it's silly solution, but adding not fully working plugins to vite is not an option for me. So in meantime until Vue 3.3 will be ready I just define types used in props in component and export them. Then this type is imported in types.ts and reexported. This way I still import all types elsewhere from 'types.ts', so when vue 3.3 will be ready, I just move definitions to that file and the rest of the code stays untouched.

In FooField.vue:

<script setup lang="ts">
export type FooFieldProps = {
  foo: string
  bar?: string
}
defineProps<FooFieldProps>()
</script>

then in types.ts:

export type { FooFieldProps } from './FooField.vue'

👍Good wit

None of the proposed workarounds really worked for all uses cases but it really makes things easier to pass around the prop types to call some shared functions. I don't have a ton of props so I just ended up declaring them twice, once as an interface and once as a PropType. To keep the maintenance of this redundant code easier I came up with these helpers that keep the two in sync:

import {PropType} from "vue";
// copied and slightly modified from vue core
declare type Data = Record<string, unknown>;
declare type DefaultFactory<T> = (props: Data) => T | null | undefined;

type OptionalRequired<T> = undefined extends T ? { required?: false } : { required: true };

declare interface PropOptionsWorkaroundRequired<T = any, D = T> {
    type?: PropType<T> | true | null;
    default?: D | DefaultFactory<D> | null | undefined | object;

    validator?(value: unknown): boolean;
}

// convert interface properties to property options
type PropsWorkaround<T> = {
    [Key in keyof T]-?: PropOptionsWorkaroundRequired<T[Key]> & OptionalRequired<T[Key]>
}

to be used like this

interface InterfaceType {
    foo: string
}

export interface PropsInterface {
    text: string,
    textOptional?: string,
    someNumber?: number,
    interface: InterfaceType
}

export const props_from_interface: PropsWorkaround<PropsInterface> = {
    text: {type: String, required: true},
    textOptional: {type: String},
    someNumber: {type: Number},
    interface: {type: Object as PropType<InterfaceType>, required: true}
}

// and somewhere else
import {props_from_interface} from "file"
const props = defineProps(props_from_interface);

Of course it is less than ideal to have redundant code but at least this way props_from_interface will not compile if any properties from PropsInterface are not implemented. And it is easy enough to add a new property in two places instead of just one until a fix lands in vue.

when this type will be used in other files.

// types.ts
interface Test {
  a: string;
}

// component.vue
<script lang="ts" setup>
  import type { Test } from 'types';
  type Props<T extends unkown> = {
    [P in keyof T]: T[P];
  }
 defineProps<Props<Test>>()
</script>

// other.ts
import type { Test } from 'types';
// ...

Any progress on a real fix for this issue?

Any progress on a real fix for this issue?

AFAIK this is on the roadmap for the vue 3.3 release

2022.09.21 这个问题依然存在🙈

Will the fix for this get back ported to 2.7?

We keep our composables and components separate (makes testing VERY easy). It would make for much cleaner code if the prop type and emit types were declared in the composable file.

Example of what we do:

https://stackoverflow.com/questions/72890865/how-to-test-computed-value-inside-setup-function-in-vue-js-3-with-vue-test-utils/73393548#73393548

So, the typescript in Vue3.x is a fake concept;

I first found the props concept can work in Vue3.x along with ts that way, which make me feel like home at first glance;

For example, in foo.vue;

import { defineProps } from 'vue'

interface Props {
    foo: string;
    bar?: number;
}
const props = defineProps<Props>();

However; while I am trying make a little step over, like called my own interface existed in another Props.ts file.

export interface Props {
    foo: string;
    bar?: number;
}

Then, import it to make everything more clear;

import { defineProps } from 'vue'
import Props from '@/utils/Props'

const props = defineProps<Props>();

Vue3.x shows everything just fucks up here:

type argument passed to defineProps() must be a literal type, or a reference to an interface or literal type.

@Mon-ius The entire discussion is about just that.

Interfaces work until you put them in a different file ant try to import... Tried with type import regular import with export default with export nothing helps always:

[vite] Internal server error: [@vue/compiler-sfc] type argument passed to defineProps() must be a literal type, or a reference to an interface or literal type.
src/components/TheContent.vue
2 | import type WizardSteps from "@/interfaces/WizardSteps";
3 |
4 | defineProps();
| ^^^^^^^^^^^
5 | </script>
6 |
Plugin: vite:vue
src/components/TheContent.vue
at error (node_modules/.pnpm/@VUE+compiler-sfc@3.2.40/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js:3589:15)
at processDefineProps (node_modules/.pnpm/@VUE+compiler-sfc@3.2.40/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js:3631:17)
at Object.compileScript (node_modules/.pnpm/@VUE+compiler-sfc@3.2.40/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js:4105:17)
at resolveScript (file://node_modules/.pnpm/@vitejs+plugin-vue@3.1.0_vite@3.1.4+vue@3.2.40/node_modules/@vitejs/plugin-vue/dist/index.mjs:266:31)
at genScriptCode (file:///node_modules/.pnpm/@vitejs+plugin-vue@3.1.0_vite@3.1.4+vue@3.2.40/node_modules/@vitejs/plugin-vue/dist/index.mjs:2346:18)
at transformMain (file:///node_modules/.pnpm/@vitejs+plugin-vue@3.1.0_vite@3.1.4+vue@3.2.40/node_modules/@vitejs/plugin-vue/dist/index.mjs:2167:54)
at TransformContext.transform (file:///node_modules/.pnpm/@vitejs+plugin-vue@3.1.0_vite@3.1.4+vue@3.2.40/node_modules/@vitejs/plugin-vue/dist/index.mjs:2642:16)
at Object.transform (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:41103:44)
at async loadAndTransform (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:37365:29)

Maybe it's silly solution, but adding not fully working plugins to vite is not an option for me. So in meantime until Vue 3.3 will be ready I just define types used in props in component and export them. Then this type is imported in types.ts and reexported. This way I still import all types elsewhere from 'types.ts', so when vue 3.3 will be ready, I just move definitions to that file and the rest of the code stays untouched.

In FooField.vue:

<script setup lang="ts">
export type FooFieldProps = {
  foo: string
  bar?: string
}
defineProps<FooFieldProps>()
</script>

then in types.ts:

export type { FooFieldProps } from './FooField.vue'

I get for export modifier, when it is using setup attribute in script:

TS1184: Modifiers cannot appear here.

I guess I'll stick with duplication of interfaces for time being, and later on will move them into their files. 🙂

Also there is problem with alias in vite.config.ts and tsconfig.json, it does not work:

import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path';
import { VitePluginFonts } from 'vite-plugin-fonts'

// https://vitejs.dev/config/
export default defineConfig({
    resolve: {
        alias: {
            '@/': path.resolve(__dirname, './src')
        },
    },
    plugins: [
        VitePluginFonts({
          google: {
            preconnect: true,
            display: 'swap',
            injectTo: 'head-prepend',
            families: [
              {
                name: 'Montserrat',
                styles: 'ital,wght@0,400;0,800;1,200',
                defer: true,
              },
            ],
          },
        }),
        vue()
    ]
})

and tsconfig.json:

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    "baseUrl": "src",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Output:

Failed to resolve import "@/components/TheHeader.vue" from "src/App.vue". Does the file exist?
4:54:40 PM [vite] Internal server error: Failed to resolve import "@/components/TheHeader.vue" from "src/App.vue". Does the file exist?
  Plugin: vite:import-analysis
  File: src/App.vue
  1  |  import { defineComponent as _defineComponent } from "vue";
  2  |  import TheHeader from "@/components/TheHeader.vue";
     |                         ^
  3  |  import TheContent from "@/components/TheContent.vue";
  4  |  const _sfc_main = /* @__PURE__ */ _defineComponent({
      at formatError (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:40854:46)
      at TransformContext.error (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:40850:19)
      at normalizeUrl (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:37587:33)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async TransformContext.transform (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:37720:47)
      at async Object.transform (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:41103:30)
      at async loadAndTransform (file:///node_modules/.pnpm/vite@3.1.4/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:37365:29)