# Vue 3 con TypeScript en 2026: props, emits y composables con tipos seguros
> Patrones de Vue 3 y TypeScript para 2026: defineProps, defineEmits, composables, defineModel y provide/inject con tipos seguros, ejemplos prácticos y preguntas de entrevista.
- Published: 2026-07-07
- Updated: 2026-07-07
- Author: SharpSkill
- Tags: Vue 3, TypeScript, defineProps, Composables, Frontend
- Reading time: 8 min
---
Vue 3 con TypeScript en 2026 ofrece a los componentes una verificación estática completa de tipos en props, eventos y lógica reutilizable. La sintaxis `
{{ props.name }}
{{ props.role }}
```
La prop `role` queda ahora restringida a dos cadenas literales, de modo que pasar `role="guest"` hace fallar la compilación. Esta es la razón principal para recurrir a `defineProps` con TypeScript: la [guía oficial de TypeScript de Vue](https://vuejs.org/guide/typescript/composition-api.html) trata la forma genérica como la opción por defecto en los proyectos con `
```
Vue 3.5 estabilizó la **desestructuración reactiva de props**, que hoy es el patrón más conciso. Desestructurar el resultado de `defineProps` y asignar un valor por defecto en la misma instrucción sigue siendo totalmente reactivo: el compilador reescribe cada acceso hacia `props.x` por debajo.
```vue
{{ label }}
```
> **La trampa de reactividad con props desestructuradas**
>
> Las props desestructuradas siguen siendo reactivas en el template y en un `computed`, pero pasar un valor desestructurado directamente a `watch` o a un composable lo lee una sola vez y rompe el enlace reactivo. Hay que envolverlo en un getter — `watch(() => color, ...)` — o convertirlo con `toRef(props, 'color')` cuando se necesita una ref más adelante.
Es una trampa habitual en las entrevistas: los candidatos suponen que la variable desestructurada es un valor plano, cuando en realidad el compilador ha redirigido cada lectura hacia `props.color`.
## Emits con tipos seguros mediante defineEmits
Los eventos merecen el mismo rigor que las props. La forma genérica de `defineEmits` describe cada nombre de evento y su carga útil como una tupla, dando al componente padre autocompletado y al componente hijo la garantía, en tiempo de compilación, de que se emiten los argumentos correctos.
```vue
```
Esta forma de tupla reemplazó la antigua sintaxis por firma de llamada (`(e: 'search', q: string): void`) porque se lee mejor y admite varios eventos sin sobrecargas. Cuando un evento no transporta datos, una tupla vacía `[]` lo documenta de forma explícita. Combinar emits tipados con props tipadas produce componentes cuya interfaz pública completa es verificable antes de la ejecución: la misma disciplina que se aborda en la [guía de la API de composición de Vue](/blog/vue-nuxt/vue-3-composition-api-complete-guide).
## Tipar composables para lógica reutilizable
Los composables son funciones normales, así que siguen las reglas ordinarias de TypeScript, pero unas pocas convenciones los mantienen ergonómicos. Conviene anotar explícitamente los tipos `Ref` cuando la inferencia no es evidente, y usar genéricos cuando un composable envuelve datos arbitrarios como la respuesta de una API.
```ts
// useFetch.ts
import { ref, type Ref } from 'vue'
interface UseFetchReturn {
data: Ref
error: Ref
loading: Ref
}
// Generic flows through to the caller's typed data
export function useFetch(url: string): UseFetchReturn {
const data = ref(null) as Ref
const error = ref(null)
const loading = ref(true)
fetch(url)
.then((r) => r.json())
.then((json: T) => { data.value = json })
.catch((e: Error) => { error.value = e })
.finally(() => { loading.value = false })
return { data, error, loading }
}
```
En el punto de llamada, el parámetro genérico hace que `data` quede totalmente tipado sin ninguna anotación adicional:
```ts
// UserList.vue (script setup)
interface User { id: number; name: string }
// data is Ref — inferred from the generic
const { data: users, loading } = useFetch('/api/users')
```
Anotar explícitamente el objeto de retorno (`UseFetchReturn`) vale esas pocas líneas extra: documenta el contrato, evita fugas accidentales de refs internas y da a los consumidores un único tipo que importar. Para patrones de composables más avanzados, como las sobrecargas de argumentos y la limpieza ligada al ciclo de vida, consulta la [guía avanzada de composables de Vue](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns).
## Tipar defineModel para el enlace bidireccional
`defineModel`, estable desde Vue 3.4, condensa el antiguo par de prop `modelValue` más emit `update:modelValue` en una única ref escribible. Su parámetro genérico tipa ambos sentidos del enlace a la vez.
```vue
```
Los modelos con nombre — `defineModel('title')` — se corresponden con `v-model:title` y reciben el mismo tipado. Esto elimina toda una categoría de errores de carga útil incoherente que el patrón manual de prop y emit solía ocultar.
## Tipar refs de template e instancias de componente
Acceder a un nodo del DOM o a un componente hijo mediante una `ref` es donde el código Vue sin tipos recae con más frecuencia en `any`. La solución consiste en parametrizar `useTemplateRef` (Vue 3.5+) o la propia `ref` con el tipo del elemento, de modo que el acceso a las propiedades se verifique contra la interfaz real del DOM.
```vue
```
Para una referencia a un componente hijo, `InstanceType` extrae el tipo público del componente, exponiendo todo lo que el hijo declaró mediante `defineExpose`. Así, las llamadas a métodos del padre hacia el hijo quedan totalmente verificadas en lugar de adivinadas.
```vue
```
## provide e inject con tipos seguros usando InjectionKey
La inyección de dependencias a través del árbol de componentes pierde la información de tipos a menos que la clave la transporte. `InjectionKey` es un símbolo tipado que vincula el tipo de un valor a su clave, de modo que `provide` e `inject` permanecen sincronizados sin casts manuales.
```ts
// theme-key.ts
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
mode: Ref<'light' | 'dark'>
toggle: () => void
}
// The key permanently associates the ThemeContext type with this symbol
export const ThemeKey: InjectionKey = Symbol('theme')
```
```ts
// provider (script setup) — value must match ThemeContext or it fails to compile
provide(ThemeKey, { mode, toggle })
// consumer — theme is inferred as ThemeContext | undefined
const theme = inject(ThemeKey)
theme?.toggle()
```
Proporcionar un valor cuya forma no coincida con `ThemeContext` provoca un error de compilación en el punto de inyección, capturando cualquier desajuste entre componentes distantes antes de que llegue a producción.
## Preguntas frecuentes de entrevista sobre Vue y TypeScript
Los entrevistadores sondean si el candidato comprende la frontera entre los tipos de compilación y el comportamiento en tiempo de ejecución. Algunas preguntas recurrentes y sus respuestas precisas:
- **¿Por qué preferir `defineProps()` a la forma objeto de ejecución?** Las props genéricas expresan tipos unión, firmas de función y estructuras anidadas que las declaraciones de ejecución no pueden capturar, y eliminan la duplicación entre la definición de tipo y la de ejecución.
- **¿Son reactivas las props desestructuradas?** Sí desde Vue 3.5+, porque el compilador reescribe el acceso hacia `props.x`. Pero un *valor* desestructurado pasado a `watch` o a un composable se lee una sola vez: hay que usar un getter o `toRef`.
- **¿Cómo se tipa un evento emitido sin carga útil?** Con una tupla vacía: `defineEmits<{ close: [] }>()`.
- **¿Qué reemplaza `defineModel`?** El par de prop `modelValue` y emit `update:modelValue`, unificado en una única ref escribible tipada.
Practicar con un banco de preguntas real afina los reflejos que las entrevistas premian: el [módulo de entrevista sobre composables de Vue](/technologies/vue-nuxt/interview-questions/vue-composables) entrena precisamente estos patrones. El instrumental también importa: ejecutar `vue-tsc` en la CI para que los errores de tipos bloqueen los merges, y apoyarse en el [manual de TypeScript](https://www.typescriptlang.org/docs/handbook/2/generics.html) cuando entran en juego los genéricos de los composables. La experiencia en el editor se apoya en [el instrumental oficial Volar de Vue](https://github.com/vuejs/language-tools), que lee estas macros para señalar los errores en línea.
## Conclusión
- Usar la forma genérica `defineProps()` para expresar tipos unión, campos opcionales y props callback que las declaraciones de ejecución no pueden capturar.
- Preferir la desestructuración reactiva de props con valores por defecto en línea desde Vue 3.5+, y recurrir a `withDefaults` solo cuando un objeto de valores por defecto compartido resulte más claro.
- Tipar los eventos con la forma de tupla de `defineEmits` para que las cargas útiles se verifiquen en tiempo de compilación tanto en el hijo como en el padre.
- Anotar explícitamente los tipos de retorno de los composables y usar genéricos para reenviar de extremo a extremo los tipos de datos que proporciona quien llama.
- Adoptar `defineModel()` para el enlace bidireccional y así condensar el boilerplate de prop más emit en una única ref tipada.
- Ejecutar `vue-tsc` en integración continua para que las regresiones de tipos hagan fallar la compilación en lugar de llegar a producción.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/es/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables