Vue 3 Script Setup y defineModel en 2026: Sintaxis Moderna y Preguntas de Entrevista
Guía completa sobre la sintaxis script setup de Vue 3 y defineModel. Exploración de patrones modernos, TypeScript y preparación para entrevistas técnicas de Vue.js.

La sintaxis script setup de Vue 3 transforma la manera en que se escriben los componentes al eliminar el código repetitivo y mejorar la integración con TypeScript. Combinada con defineModel, introducido experimentalmente en Vue 3.3 y estabilizado en Vue 3.4, la construcción de componentes con enlace bidireccional de datos requiere mucho menos código que el equivalente con Options API.
A partir de Vue 3.5, las props pueden desestructurarse directamente desde defineProps manteniendo la reactividad. Esto elimina la necesidad de toRefs en la mayoría de los casos.
Fundamentos de Script Setup en Vue 3.5
La sintaxis <script setup> es azúcar sintáctica en tiempo de compilación para la Composition API dentro de los Single File Components. Las variables declaradas en el nivel superior se exponen automáticamente al template, y las importaciones son utilizables directamente sin necesidad de registro.
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'
// Props con interfaz TypeScript
const props = defineProps<{
user: User
showEmail?: boolean
}>()
// Estado reactivo
const isExpanded = ref(false)
// Propiedad computada
const displayName = computed(() => {
return `${props.user.firstName} ${props.user.lastName}`
})
// Emisor de eventos con payload tipado
const emit = defineEmits<{
select: [userId: string]
delete: [userId: string]
}>()
// Los métodos son funciones simples
function handleSelect() {
emit('select', props.user.id)
}
</script>
<template>
<div class="user-card" @click="handleSelect">
<h3>{{ displayName }}</h3>
<p v-if="showEmail">{{ user.email }}</p>
<button @click.stop="isExpanded = !isExpanded">
{{ isExpanded ? 'Contraer' : 'Expandir' }}
</button>
</div>
</template>El compilador maneja automáticamente las declaraciones de exportación y el registro de componentes. Esto reduce la carga cognitiva en comparación con retornar explícitamente valores desde una función setup().
Desestructuración Reactiva de Props (Vue 3.5+)
Antes de Vue 3.5, la desestructuración de props rompía la reactividad porque la desestructuración de JavaScript crea una instantánea del valor en ese momento. Vue 3.5 introduce un seguimiento a nivel del compilador que preserva la reactividad para las props desestructuradas.
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'
// Desestructuración con valores por defecto - permanece reactiva en 3.5+
const { query = '', placeholder = 'Buscar...' } = defineProps<{
query?: string
placeholder?: string
}>()
// Watch funciona en props desestructuradas
watch(
() => query,
(newQuery) => {
console.log('Query modificada:', newQuery)
}
)
</script>
<template>
<input
:value="query"
:placeholder="placeholder"
@input="$emit('update:query', ($event.target as HTMLInputElement).value)"
/>
</template>Esta sintaxis requiere Vue 3.5 o posterior. El anuncio de Vue 3.5 documenta la lista completa del comportamiento de desestructuración reactiva de props.
defineModel para Enlace Bidireccional
Antes de defineModel, implementar v-model en un componente personalizado requería declarar una prop y emitir manualmente un evento de actualización. Este patrón aparece en innumerables bases de código Vue:
<!-- La forma antigua - verbosa pero explícita -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
function updateValue(event: Event) {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>Con defineModel, el mismo componente se vuelve significativamente más corto:
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel crea una ref que se sincroniza con v-model
const model = defineModel<string>({ default: '' })
</script>
<template>
<input
v-model="model"
type="text"
class="text-input"
/>
</template>El componente padre usa la sintaxis estándar de v-model:
<!-- ParentForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import TextInput from './TextInput.vue'
const username = ref('')
</script>
<template>
<TextInput v-model="username" />
<p>Nombre de usuario: {{ username }}</p>
</template>defineModel retorna una ref que permanece sincronizada con el valor enlazado del padre. Las mutaciones a esta ref emiten automáticamente el evento de actualización.
¿Listo para aprobar tus entrevistas de Vue.js / Nuxt.js?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Modelos Nombrados y Enlaces v-model Múltiples
Los componentes a veces necesitan múltiples enlaces bidireccionales. Vue soporta enlaces v-model nombrados, y defineModel los maneja aceptando un argumento de nombre.
<!-- DateRangePicker.vue -->
<script setup lang="ts">
import { computed } from 'vue'
// Modelos nombrados para múltiples enlaces v-model
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })
// Validación computada
const isValidRange = computed(() => {
if (!startDate.value || !endDate.value) return true
return startDate.value <= endDate.value
})
</script>
<template>
<div class="date-range-picker">
<input
type="date"
:value="startDate?.toISOString().split('T')[0]"
@input="startDate = new Date(($event.target as HTMLInputElement).value)"
/>
<span>hasta</span>
<input
type="date"
:value="endDate?.toISOString().split('T')[0]"
@input="endDate = new Date(($event.target as HTMLInputElement).value)"
/>
<p v-if="!isValidRange" class="error">La fecha de fin debe ser posterior a la fecha de inicio</p>
</div>
</template>El padre enlaza ambos modelos:
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />Modificadores v-model con defineModel
Los modificadores incorporados de Vue como .trim, .number y .lazy funcionan automáticamente con defineModel. Los modificadores personalizados requieren manejo explícito a través del segundo elemento de la tupla retornada.
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Acceso a modificadores via el segundo valor de retorno
const [model, modifiers] = defineModel<number>({
default: 0,
// Funciones de transformación para modificadores personalizados
set(value) {
// Redondear a 2 decimales si el modificador 'round' está presente
if (modifiers.round) {
return Math.round(value * 100) / 100
}
return value
}
})
</script>
<template>
<div class="currency-input">
<span>$</span>
<input
type="number"
step="0.01"
:value="model"
@input="model = parseFloat(($event.target as HTMLInputElement).value) || 0"
/>
</div>
</template>Uso con el modificador personalizado:
<CurrencyInput v-model.round="price" />Patrones TypeScript para Script Setup
La seguridad de tipos en componentes <script setup> se basa en genéricos pasados a las macros del compilador. El patrón varía ligeramente entre props, emits y modelos.
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'
// Props de componente genérico
const props = defineProps<{
items: T[]
selected?: T
}>()
const emit = defineEmits<{
select: [item: T]
remove: [id: string]
}>()
// Los slots también pueden tipparse
defineSlots<{
default: (props: { item: T; index: number }) => any
empty: () => any
}>()
const hasItems = computed(() => props.items.length > 0)
</script>
<template>
<ul v-if="hasItems">
<li
v-for="(item, index) in items"
:key="item.id"
:class="{ selected: selected?.id === item.id }"
@click="emit('select', item)"
>
<slot :item="item" :index="index">
{{ item.id }}
</slot>
</li>
</ul>
<div v-else>
<slot name="empty">Sin elementos</slot>
</div>
</template>Los componentes genéricos, disponibles desde Vue 3.3, permiten componentes tipados reutilizables sin sacrificar la inferencia de tipos en el código consumidor. Para patrones más profundos, la documentación de Vue sobre TypeScript cubre los casos límite.
Preguntas de Entrevista Comunes sobre Script Setup
Los entrevistadores que evalúan conocimientos de Vue a menudo se enfocan en las diferencias entre script setup y Options API, así como en los mecanismos de reactividad.
Pregunta: ¿Por qué la desestructuración de props rompe la reactividad, y cómo lo resuelve Vue 3.5?
En JavaScript, la desestructuración crea nuevas variables que contienen los valores en el momento de la desestructuración. Si props.count cambia después, una const { count } = props desestructurada aún mantiene el valor antiguo. El compilador de Vue 3.5 transforma las variables desestructuradas en getters que se reevalúan al accederse, preservando la reactividad sin sobrecarga en tiempo de ejecución.
Pregunta: ¿Qué sucede internamente cuando se usa defineModel?
El compilador expande defineModel en una declaración de prop y un registro de emit. En tiempo de ejecución, crea una ref cuyo getter lee el valor de la prop y cuyo setter llama emit('update:modelValue', newValue). Esta ref no es una copia verdadera de la prop sino un proxy que reenvía lecturas y escrituras apropiadamente.
Pregunta: ¿Cuándo debería un componente usar script setup versus una función setup regular?
Script setup es preferible para la mayoría de los single-file components porque reduce el boilerplate y mejora el soporte del IDE. Una función setup() regular es necesaria cuando el componente necesita exponer métodos a componentes padres vía defineExpose en patrones complejos, o al construir componentes renderless que retornan funciones de render directamente. Para preparación de entrevistas, explora más patrones de composables Vue.
Migrando de Options API a Script Setup
Las bases de código existentes a menudo contienen componentes Options API que se benefician de la migración. La conversión sigue un patrón predecible:
<!-- Antes: Options API -->
<script lang="ts">
import { defineComponent, PropType } from 'vue'
export default defineComponent({
props: {
items: { type: Array as PropType<string[]>, required: true }
},
emits: ['select'],
data() {
return { searchTerm: '' }
},
computed: {
filteredItems() {
return this.items.filter(item =>
item.toLowerCase().includes(this.searchTerm.toLowerCase())
)
}
},
methods: {
selectItem(item: string) {
this.$emit('select', item)
}
}
})
</script><!-- Después: Script Setup -->
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = defineProps<{ items: string[] }>()
const emit = defineEmits<{ select: [item: string] }>()
const searchTerm = ref('')
const filteredItems = computed(() =>
props.items.filter(item =>
item.toLowerCase().includes(searchTerm.value.toLowerCase())
)
)
function selectItem(item: string) {
emit('select', item)
}
</script>La versión migrada es más corta y la inferencia de tipos funciona sin casts PropType. El equipo de Vue proporciona vue-codemod para migraciones automatizadas, aunque la revisión manual sigue siendo necesaria para componentes complejos.
Consideraciones de Rendimiento en Script Setup
Los componentes script setup compilan a código más eficiente porque el compilador conoce la forma exacta del componente en tiempo de compilación. Específicamente:
- Los enlaces de template se resuelven en tiempo de compilación en lugar de búsqueda de propiedades en tiempo de ejecución
- Las importaciones no utilizadas se eliminan más efectivamente mediante tree-shaking
- La ausencia de una función envolvente reduce la sobrecarga de closures
Para aplicaciones que priorizan el tamaño del bundle, script setup con Vapor Mode (disponible en Vue 3.6+) elimina completamente el DOM virtual para los componentes que optan por él, generando operaciones DOM directas en su lugar.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Puntos Clave para Vue 3 Script Setup y defineModel
- Script setup reduce el boilerplate de componentes al exponer automáticamente los bindings de nivel superior a los templates y manejar el registro de componentes
defineModelreemplaza el patrón manual prop + emit parav-model, retornando una ref que se sincroniza con el valor enlazado del padre- Vue 3.5 introduce la desestructuración reactiva de props, permitiendo
const { prop } = defineProps()sin perder reactividad - Los modelos nombrados soportan múltiples enlaces
v-modelen un solo componente usandodefineModel('name') - Los genéricos TypeScript funcionan con
defineProps,defineEmits,defineSlots, y componentes genéricos vía el atributogeneric - La migración desde Options API sigue un patrón mecánico: data se convierte en refs, computed permanece como computed, los métodos se vuelven funciones, y el compilador maneja los exports
¿Sabrías detectar el bug en Vue.js / Nuxt.js?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 18 de septiembre de 2026
Etiquetas
Compartir
Artículos relacionados

Vue 3 Reactivity Transform en 2026: $ref, $computed y Preguntas de Entrevista
Guía completa sobre Reactivity Transform de Vue 3, incluyendo $ref, $computed, macros de reactividad y preguntas frecuentes en entrevistas técnicas.

Vue 3 Teleport y Suspense: Patrones Avanzados y Preguntas de Entrevista 2026
Domina Teleport y Suspense en Vue 3.5 con patrones de producción, manejo de errores y las preguntas de entrevista técnica más frecuentes para desarrolladores Vue.

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.