Vue 3 Script Setup e defineModel nel 2026: Sintassi Moderna e Domande di Colloquio

Guida completa a Vue 3 script setup e defineModel per il data binding bidirezionale. Copre la destrutturazione reattiva dei props, l'integrazione TypeScript e le domande comuni nei colloqui tecnici.

Tutorial Vue 3 Script Setup e defineModel

La sintassi Vue 3 Script Setup trasforma il modo in cui vengono scritti i componenti, eliminando il boilerplate e migliorando l'integrazione con TypeScript. Combinata con defineModel, introdotto sperimentalmente in Vue 3.3 e stabilizzato in Vue 3.4, la creazione di componenti con data binding bidirezionale richiede molto meno codice rispetto all'equivalente Options API.

Vue 3.5 Destrutturazione Reattiva dei Props

A partire da Vue 3.5, i props possono essere destrutturati direttamente da defineProps mantenendo la reattività. Questo elimina la necessità di toRefs nella maggior parte dei casi.

Fondamenti di Script Setup in Vue 3.5

La sintassi <script setup> è uno zucchero sintattico a tempo di compilazione per la Composition API all'interno dei Single File Components. Le variabili dichiarate al livello superiore sono automaticamente esposte al template, e gli import sono direttamente utilizzabili senza registrazione.

vue
<!-- UserCard.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User } from '@/types'

// Props con interfaccia TypeScript
const props = defineProps<{
  user: User
  showEmail?: boolean
}>()

// Stato reattivo
const isExpanded = ref(false)

// Proprietà computed
const displayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

// Event emitter con payload tipizzato
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// I metodi sono semplici funzioni
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 ? 'Comprimi' : 'Espandi' }}
    </button>
  </div>
</template>

Il compilatore gestisce automaticamente gli statement di export e la registrazione dei componenti. Questo riduce il carico cognitivo rispetto al ritorno esplicito dei valori da una funzione setup().

Destrutturazione Reattiva dei Props (Vue 3.5+)

Prima di Vue 3.5, la destrutturazione dei props interrompeva la reattività perché la destrutturazione JavaScript crea uno snapshot del valore in quel momento. Vue 3.5 introduce il tracking a livello di compilatore che preserva la reattività per i props destrutturati.

vue
<!-- SearchInput.vue -->
<script setup lang="ts">
import { watch } from 'vue'

// Destrutturazione con default - rimane reattiva in 3.5+
const { query = '', placeholder = 'Cerca...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch funziona sui props destrutturati
watch(
  () => query,
  (newQuery) => {
    console.log('Query cambiata:', newQuery)
  }
)
</script>

<template>
  <input
    :value="query"
    :placeholder="placeholder"
    @input="$emit('update:query', ($event.target as HTMLInputElement).value)"
  />
</template>

Questa sintassi richiede Vue 3.5 o successivo. L'annuncio di rilascio di Vue 3.5 documenta il comportamento completo della destrutturazione reattiva dei props.

defineModel per il Binding Bidirezionale

Prima di defineModel, implementare v-model su un componente personalizzato richiedeva la dichiarazione di un prop e l'emissione manuale di un evento di aggiornamento. Questo pattern appare in innumerevoli codebase Vue:

vue
<!-- Il vecchio modo - verboso ma esplicito -->
<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, lo stesso componente diventa significativamente più breve:

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel crea un ref che si sincronizza con v-model
const model = defineModel<string>({ default: '' })
</script>

<template>
  <input
    v-model="model"
    type="text"
    class="text-input"
  />
</template>

Il componente genitore utilizza la sintassi standard 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>Nome utente: {{ username }}</p>
</template>

defineModel restituisce un ref che rimane sincronizzato con il valore legato del genitore. Le mutazioni a questo ref emettono automaticamente l'evento di aggiornamento.

Pronto a superare i tuoi colloqui su Vue.js / Nuxt.js?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Model Nominati e Binding v-model Multipli

I componenti a volte necessitano di binding bidirezionali multipli. Vue supporta binding v-model nominati, e defineModel li gestisce accettando un argomento nome.

vue
<!-- DateRangePicker.vue -->
<script setup lang="ts">
import { computed } from 'vue'

// Model nominati per binding v-model multipli
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Computed di validazione
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>a</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 data di fine deve essere successiva alla data di inizio</p>
  </div>
</template>

Il genitore lega entrambi i model:

vue
<DateRangePicker v-model:start="filterStart" v-model:end="filterEnd" />

Modificatori v-model con defineModel

I modificatori integrati di Vue come .trim, .number e .lazy funzionano automaticamente con defineModel. I modificatori personalizzati richiedono una gestione esplicita attraverso il secondo elemento della tupla restituita.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Accesso ai modificatori tramite il secondo valore di ritorno
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Funzioni di trasformazione per modificatori personalizzati
  set(value) {
    // Arrotonda a 2 decimali se è presente il modificatore 'round'
    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>

Utilizzo con il modificatore personalizzato:

vue
<CurrencyInput v-model.round="price" />

Pattern TypeScript per Script Setup

La type safety nei componenti <script setup> si basa sui generics passati alle macro del compilatore. Il pattern varia leggermente tra props, emits e model.

vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: string }">
import { computed } from 'vue'

// Props del componente generico
const props = defineProps<{
  items: T[]
  selected?: T
}>()

const emit = defineEmits<{
  select: [item: T]
  remove: [id: string]
}>()

// Anche gli slot possono essere tipizzati
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">Nessun elemento</slot>
  </div>
</template>

I componenti generici, disponibili da Vue 3.3, permettono componenti tipizzati riutilizzabili senza sacrificare l'inferenza dei tipi nel codice consumatore.

Domande Comuni nei Colloqui su Script Setup

Gli intervistatori che testano la conoscenza di Vue si concentrano spesso sulle differenze tra Script Setup e l'Options API, così come sulla meccanica della reattività.

Domanda: Perché la destrutturazione dei props interrompe la reattività e come risolve Vue 3.5?

In JavaScript, la destrutturazione crea nuove variabili che contengono i valori al momento della destrutturazione. Se props.count cambia successivamente, un const { count } = props destrutturato mantiene ancora il vecchio valore. Il compilatore di Vue 3.5 trasforma le variabili destrutturate in getter che ri-valutano quando vi si accede, preservando la reattività senza overhead runtime.

Domanda: Cosa succede sotto il cofano quando viene usato defineModel?

Il compilatore espande defineModel in una dichiarazione di prop e una registrazione di emit. A runtime, crea un ref il cui getter legge il valore del prop e il cui setter chiama emit('update:modelValue', newValue). Questo ref non è una vera copia del prop ma un proxy che inoltra letture e scritture appropriatamente.

Domanda: Quando un componente dovrebbe usare Script Setup rispetto a una funzione setup regolare?

Script Setup è preferito per la maggior parte dei Single File Components perché riduce il boilerplate e migliora il supporto IDE. Una funzione setup() regolare è necessaria quando il componente deve esporre metodi ai componenti genitori tramite defineExpose in pattern complessi, o quando si costruiscono componenti renderless che restituiscono funzioni di render direttamente.

Migrazione da Options API a Script Setup

Le codebase esistenti contengono spesso componenti Options API che beneficiano dalla migrazione. La conversione segue un pattern prevedibile:

vue
<!-- Prima: 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
<!-- Dopo: 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 versione migrata è più breve e l'inferenza dei tipi funziona senza cast PropType. Il team Vue fornisce vue-codemod per migrazioni automatizzate, sebbene la revisione manuale rimanga necessaria per componenti complessi.

Considerazioni sulle Performance in Script Setup

I componenti Script Setup compilano in codice più efficiente perché il compilatore conosce la forma esatta del componente al momento della build. Specificamente:

  • I binding del template si risolvono a tempo di compilazione piuttosto che attraverso property lookup a runtime
  • Gli import non utilizzati vengono eliminati più efficacemente tramite tree-shaking
  • L'assenza di una funzione wrapper riduce l'overhead delle closure

Per le applicazioni che danno priorità alla dimensione del bundle, Script Setup con Vapor Mode (disponibile da Vue 3.6+) elimina completamente il DOM virtuale per i componenti opt-in, generando invece operazioni DOM dirette.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Punti Chiave su Vue 3 Script Setup e defineModel

  • Script Setup riduce il boilerplate dei componenti esponendo automaticamente i binding di livello superiore ai template e gestendo la registrazione dei componenti
  • defineModel sostituisce il pattern manuale prop + emit per v-model, restituendo un ref che si sincronizza con il valore legato del genitore
  • Vue 3.5 introduce la destrutturazione reattiva dei props, permettendo const { prop } = defineProps() senza perdere la reattività
  • I model nominati supportano binding v-model multipli su un singolo componente usando defineModel('name')
  • L'integrazione TypeScript migliora con componenti generici, emit tipizzati e supporto slot tipizzato attraverso le macro del compilatore
Sfida del giorno

Sapresti trovare il bug in Vue.js / Nuxt.js?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 18 settembre 2026

Tag

#vue
#vue3
#script-setup
#definemodel
#composition-api
#typescript

Condividi

Articoli correlati