# 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 ` ``` 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 ``` > **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