Type-based defineProps<T>() works with imported interfaces and type aliases since Vue 3.3, but some complex TypeScript patterns fail at compile time because Vue's compiler resolves types statically, not through the full TypeScript type system.
What works
Simple interfaces, type aliases, union literals, and generics:
// types/props.ts
export interface User {
id: number
name: string
email?: string
}
export type Status = 'pending' | 'active' | 'completed'<script setup lang="ts">
import type { User, Status } from '@/types/props'
defineProps<{
user: User
status: Status
items: string[]
}>()
</script>Using an imported interface directly as the props type also works:
<script setup lang="ts">
import type { User } from '@/types/props'
defineProps<User>()
</script>What doesn't work
Conditional types as the entire props shape
export type InputProps<T> = T extends string
? { value: string; onChange: (v: string) => void }
: { value: number; onChange: (v: number) => void }<script setup lang="ts">
// ERROR: Vue can't resolve conditional types for the props object
defineProps<InputProps<string>>()
</script>Fix: resolve the type manually:
<script setup lang="ts">
interface StringInputProps {
value: string
onChange: (v: string) => void
}
defineProps<StringInputProps>()
</script>Complex mapped types
export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}<script setup lang="ts">
// May fail or produce incorrect runtime declarations
defineProps<DeepReadonly<User>>()
</script>Fix: flatten the type into an explicit interface:
export interface ReadonlyUser {
readonly id: number
readonly name: string
readonly email?: string
}Global ambient types (no export)
// global.d.ts
interface AppConfig {
theme: string
locale: string
}<script setup lang="ts">
// ERROR: "Unresolvable type reference"
defineProps<{ config: AppConfig }>()
</script>Fix: use explicit exports and imports instead of ambient declarations:
// types/config.ts
export interface AppConfig {
theme: string
locale: string
}Types from node_modules (sometimes)
Some complex types from external packages can fail if they use advanced generics or conditional types internally. The workaround is the same: create a local interface that flattens the external type.
The general rule
If Vue's compiler can statically resolve the type to a set of property names with concrete types, it works. If the type requires runtime evaluation, conditional branching, or recursive type resolution, it fails.
Works: interfaces, type aliases, union literals, Pick, Omit, Partial, Required, simple generics.
Fails: conditional types (T extends X ? A : B), deeply recursive mapped types, ambient globals, some cross-package types.
Dual script block workaround
For cases where you need complex type computation, resolve it in a regular <script> block:
<script lang="ts">
import type { ComplexGeneric } from '@/types'
type Resolved = ComplexGeneric<'variant-a'>
</script>
<script setup lang="ts">
defineProps<Resolved>()
</script>The regular script block has full TypeScript access. The resolved type is then simple enough for <script setup> to handle.
Version history
| Vue version | What's supported |
|---|---|
| 3.2 | Only inline types (no imports) |
| 3.3 | Imported interfaces, type aliases, enums |
| 3.4+ | Better support for Pick, Omit, cross-file generics |