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.

Vue 3 Script Setup y defineModel en 2026

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.

Desestructuración Reactiva de Props en Vue 3.5

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.

vue
<!-- 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.

vue
<!-- 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:

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:

vue
<!-- 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:

vue
<!-- 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.

vue
<!-- 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:

vue
<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.

vue
<!-- 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:

vue
<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.

vue
<!-- 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:

vue
<!-- 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>
vue
<!-- 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
  • defineModel reemplaza el patrón manual prop + emit para v-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-model en un solo componente usando defineModel('name')
  • Los genéricos TypeScript funcionan con defineProps, defineEmits, defineSlots, y componentes genéricos vía el atributo generic
  • 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
Reto diario

¿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.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador 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

#vue
#vue 3
#script setup
#defineModel
#composition api
#typescript
#entrevista técnica

Compartir

Artículos relacionados