defineProps<T>() basado en tipos funciona con interfaces importadas y alias de tipo desde Vue 3.3, pero algunos patrones complejos de TypeScript fallan en tiempo de compilación porque el compilador de Vue resuelve los tipos estáticamente, no a través del sistema completo de tipos de TypeScript.
Qué funciona
Interfaces simples, alias de tipo, uniones literales y genéricos:
// 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>Usar una interfaz importada directamente como tipo de props también funciona:
<script setup lang="ts">
import type { User } from '@/types/props'
defineProps<User>()
</script>Qué no funciona
Tipos condicionales como la forma completa de las props
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 no puede resolver tipos condicionales para el objeto de props
defineProps<InputProps<string>>()
</script>Solución: resuelve el tipo manualmente:
<script setup lang="ts">
interface StringInputProps {
value: string
onChange: (v: string) => void
}
defineProps<StringInputProps>()
</script>Tipos mapeados complejos
export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}<script setup lang="ts">
// Puede fallar o producir declaraciones de runtime incorrectas
defineProps<DeepReadonly<User>>()
</script>Solución: aplana el tipo en una interfaz explícita:
export interface ReadonlyUser {
readonly id: number
readonly name: string
readonly email?: string
}Tipos globales ambientales (sin export)
// global.d.ts
interface AppConfig {
theme: string
locale: string
}<script setup lang="ts">
// ERROR: "Unresolvable type reference"
defineProps<{ config: AppConfig }>()
</script>Solución: usa exports e imports explícitos en lugar de declaraciones ambientales:
// types/config.ts
export interface AppConfig {
theme: string
locale: string
}Tipos de node_modules (a veces)
Algunos tipos complejos de paquetes externos pueden fallar si internamente usan genéricos avanzados o tipos condicionales. La solución es la misma: crea una interfaz local que aplane el tipo externo.
La regla general
Si el compilador de Vue puede resolver el tipo estáticamente como un conjunto de nombres de propiedades con tipos concretos, funciona. Si el tipo requiere evaluación en runtime, ramificación condicional o resolución recursiva de tipos, falla.
Funciona: interfaces, alias de tipo, uniones literales, Pick, Omit, Partial, Required, genéricos simples.
Falla: tipos condicionales (T extends X ? A : B), tipos mapeados recursivos profundos, globals ambientales, algunos tipos entre paquetes.
Solución con bloque script dual
Para casos donde necesitas computación compleja de tipos, resuélvelo en un bloque <script> regular:
<script lang="ts">
import type { ComplexGeneric } from '@/types'
type Resolved = ComplexGeneric<'variant-a'>
</script>
<script setup lang="ts">
defineProps<Resolved>()
</script>El bloque script regular tiene acceso completo a TypeScript. El tipo resuelto es entonces suficientemente simple para que <script setup> lo gestione.
Historial de versiones
| Versión de Vue | Qué está soportado |
|---|---|
| 3.2 | Solo tipos inline (sin imports) |
| 3.3 | Interfaces importadas, alias de tipo, enums |
| 3.4+ | Mejor soporte para Pick, Omit, genéricos entre archivos |