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.

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.
À 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.
<!-- 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.
<!-- 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 :
<!-- 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 :
<!-- 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 :
<!-- 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.
<!-- 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 :
<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é.
<!-- 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é :
<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.
<!-- 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 :
<!-- 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><!-- 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
defineModelremplace le pattern manuel prop + emit pourv-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-modelsur un seul composant en utilisantdefineModel('name') - Les génériques TypeScript fonctionnent avec
defineProps,defineEmits,defineSlots, et les composants génériques via l'attributgeneric - 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
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.

Écrit par
Anthony Fillion-MailletFondateur 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
Partager
Articles similaires

Vue 3 Reactivity Transform en 2026 : $ref, $computed et Questions d'Entretien
Guide complet sur le Reactivity Transform de Vue 3, incluant $ref, $computed, les macros de réactivité et les questions d'entretien technique fréquentes.

Vue 3 Teleport et Suspense : Patterns Avancés et Questions d'Entretien 2026
Maîtrisez Teleport et Suspense dans Vue 3.5 avec des patterns de production, la gestion des erreurs et les questions d'entretien technique les plus fréquentes.

Vue 3 avec TypeScript en 2026 : props, emits et composables typés
Les patterns Vue 3 et TypeScript en 2026 : defineProps, defineEmits, composables, defineModel et provide/inject typés, avec exemples concrets et questions d'entretien.