Vue 3 Script Setup et defineModel en 2026 : Syntaxe Moderne et Questions d'Entretien

Guide complet sur la syntaxe script setup de Vue 3 et defineModel. Exploration des patterns modernes, TypeScript, et préparation aux entretiens techniques Vue.js.

Vue 3 Script Setup et defineModel en 2026

La syntaxe script setup de Vue 3 révolutionne la façon dont les composants sont écrits en éliminant le code boilerplate et en améliorant l'intégration TypeScript. Combinée avec defineModel, introduit expérimentalement dans Vue 3.3 et stabilisé dans Vue 3.4, la création de composants avec liaison bidirectionnelle nécessite beaucoup moins de code que l'équivalent Options API.

Déstructuration Réactive des Props Vue 3.5

À partir de Vue 3.5, les props peuvent être déstructurées directement depuis defineProps tout en préservant la réactivité. Cela élimine le besoin de toRefs dans la plupart des cas.

Fondamentaux du Script Setup dans Vue 3.5

La syntaxe <script setup> est un sucre syntaxique au moment de la compilation pour la Composition API à l'intérieur des Single File Components. Les variables déclarées au niveau supérieur sont automatiquement exposées au template, et les imports sont directement utilisables sans enregistrement.

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

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

// État réactif
const isExpanded = ref(false)

// Propriété calculée
const displayName = computed(() => {
  return `${props.user.firstName} ${props.user.lastName}`
})

// Émetteur d'événements avec payload typé
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// Les méthodes sont des fonctions 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 ? 'Réduire' : 'Développer' }}
    </button>
  </div>
</template>

Le compilateur gère automatiquement les déclarations d'export et l'enregistrement des composants. Cela réduit la charge cognitive par rapport au retour explicite de valeurs depuis une fonction setup().

Déstructuration Réactive des Props (Vue 3.5+)

Avant Vue 3.5, la déstructuration des props cassait la réactivité car la déstructuration JavaScript crée un instantané de la valeur à ce moment précis. Vue 3.5 introduit un suivi au niveau du compilateur qui préserve la réactivité pour les props déstructurées.

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

// Déstructuration avec valeurs par défaut - reste réactive en 3.5+
const { query = '', placeholder = 'Rechercher...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch fonctionne sur les props déstructurées
watch(
  () => query,
  (newQuery) => {
    console.log('Query modifiée:', newQuery)
  }
)
</script>

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

Cette syntaxe nécessite Vue 3.5 ou une version ultérieure. L'annonce de Vue 3.5 documente la liste complète des comportements de déstructuration réactive des props.

defineModel pour la Liaison Bidirectionnelle

Avant defineModel, implémenter v-model sur un composant personnalisé nécessitait de déclarer une prop et d'émettre manuellement un événement de mise à jour. Ce pattern apparaît dans d'innombrables bases de code Vue :

vue
<!-- L'ancienne méthode - verbeuse mais explicite -->
<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>

Avec defineModel, le même composant devient significativement plus court :

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel crée une ref qui se synchronise avec v-model
const model = defineModel<string>({ default: '' })
</script>

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

Le composant parent utilise la syntaxe 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>Nom d'utilisateur: {{ username }}</p>
</template>

defineModel retourne une ref qui reste synchronisée avec la valeur liée du parent. Les mutations de cette ref émettent automatiquement l'événement de mise à jour.

Prêt à réussir tes entretiens Vue.js / Nuxt.js ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Modèles Nommés et Liaisons v-model Multiples

Les composants ont parfois besoin de plusieurs liaisons bidirectionnelles. Vue supporte les liaisons v-model nommées, et defineModel les gère en acceptant un argument de nom.

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

// Modèles nommés pour plusieurs liaisons v-model
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Validation calculée
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>à</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 date de fin doit être après la date de début</p>
  </div>
</template>

Le parent lie les deux modèles :

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

Modificateurs v-model avec defineModel

Les modificateurs intégrés de Vue comme .trim, .number et .lazy fonctionnent automatiquement avec defineModel. Les modificateurs personnalisés nécessitent une gestion explicite via le second élément du tuple retourné.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Accès aux modificateurs via la seconde valeur de retour
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Fonctions de transformation pour les modificateurs personnalisés
  set(value) {
    // Arrondir à 2 décimales si le modificateur 'round' est présent
    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>

Utilisation avec le modificateur personnalisé :

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

Patterns TypeScript pour Script Setup

La sécurité de type dans les composants <script setup> repose sur les génériques passés aux macros du compilateur. Le pattern varie légèrement entre les props, les emits et les modèles.

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

// Props de composant générique
const props = defineProps<{
  items: T[]
  selected?: T
}>()

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

// Les slots peuvent aussi être typés
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">Aucun élément</slot>
  </div>
</template>

Les composants génériques, disponibles depuis Vue 3.3, permettent des composants typés réutilisables sans sacrifier l'inférence de type dans le code consommateur. Pour des patterns plus avancés, la documentation Vue sur TypeScript couvre les cas limites.

Questions d'Entretien Courantes sur Script Setup

Les recruteurs testant les connaissances Vue se concentrent souvent sur les différences entre script setup et l'Options API, ainsi que sur les mécanismes de réactivité.

Question : Pourquoi la déstructuration des props casse-t-elle la réactivité, et comment Vue 3.5 résout-il ce problème ?

En JavaScript, la déstructuration crée de nouvelles variables contenant les valeurs au moment de la déstructuration. Si props.count change plus tard, une const { count } = props déstructurée contient toujours l'ancienne valeur. Le compilateur de Vue 3.5 transforme les variables déstructurées en getters qui se réévaluent lors de l'accès, préservant la réactivité sans surcharge à l'exécution.

Question : Que se passe-t-il sous le capot quand defineModel est utilisé ?

Le compilateur étend defineModel en une déclaration de prop et un enregistrement d'emit. À l'exécution, il crée une ref dont le getter lit la valeur de la prop et dont le setter appelle emit('update:modelValue', newValue). Cette ref n'est pas une vraie copie de la prop mais un proxy qui transmet les lectures et écritures de manière appropriée.

Question : Quand un composant devrait-il utiliser script setup versus une fonction setup régulière ?

Script setup est préféré pour la plupart des single-file components car il réduit le boilerplate et améliore le support IDE. Une fonction setup() régulière est nécessaire quand le composant doit exposer des méthodes aux composants parents via defineExpose dans des patterns complexes, ou lors de la création de composants renderless qui retournent directement des fonctions de rendu. Pour la préparation aux entretiens, explorez plus de patterns de composables Vue.

Migration de l'Options API vers Script Setup

Les bases de code existantes contiennent souvent des composants Options API qui bénéficient d'une migration. La conversion suit un pattern prévisible :

vue
<!-- Avant : 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
<!-- Aprè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 version migrée est plus courte et l'inférence de type fonctionne sans les casts PropType. L'équipe Vue fournit vue-codemod pour les migrations automatisées, bien qu'une revue manuelle reste nécessaire pour les composants complexes.

Considérations de Performance dans Script Setup

Les composants script setup compilent vers un code plus efficace car le compilateur connaît la forme exacte du composant au moment de la compilation. Plus précisément :

  • Les liaisons de template se résolvent au moment de la compilation plutôt qu'à la recherche de propriétés à l'exécution
  • Les imports inutilisés sont éliminés plus efficacement par le tree-shaking
  • L'absence de fonction englobante réduit la surcharge des closures

Pour les applications priorisant la taille du bundle, script setup avec Vapor Mode (disponible dans Vue 3.6+) élimine entièrement le DOM virtuel pour les composants optés, générant des opérations DOM directes à la place.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Points Clés pour Vue 3 Script Setup et defineModel

  • Script setup réduit le boilerplate des composants en exposant automatiquement les liaisons de niveau supérieur aux templates et en gérant l'enregistrement des composants
  • defineModel remplace le pattern manuel prop + emit pour v-model, retournant une ref qui se synchronise avec la valeur liée du parent
  • Vue 3.5 introduit la déstructuration réactive des props, permettant const { prop } = defineProps() sans perdre la réactivité
  • Les modèles nommés supportent plusieurs liaisons v-model sur un seul composant en utilisant defineModel('name')
  • Les génériques TypeScript fonctionnent avec defineProps, defineEmits, defineSlots, et les composants génériques via l'attribut generic
  • La migration depuis l'Options API suit un pattern mécanique : data devient refs, computed reste computed, les méthodes deviennent des fonctions, et le compilateur gère les exports
Défi du jour

Tu saurais repérer le bug en Vue.js / Nuxt.js ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 18 septembre 2026

Tags

#vue
#vue 3
#script setup
#defineModel
#composition api
#typescript
#entretien technique

Partager

Articles similaires