Vue 3 Script Setup e defineModel em 2026: Sintaxe Moderna e Perguntas de Entrevista

Guia completo sobre a sintaxe script setup do Vue 3 e defineModel. Exploração de padrões modernos, TypeScript e preparação para entrevistas técnicas de Vue.js.

Vue 3 Script Setup e defineModel em 2026

A sintaxe script setup do Vue 3 transforma a maneira como os componentes são escritos ao eliminar código repetitivo e melhorar a integração com TypeScript. Combinada com defineModel, introduzido experimentalmente no Vue 3.3 e estabilizado no Vue 3.4, a construção de componentes com vinculação bidirecional de dados requer muito menos código que o equivalente com Options API.

Desestruturação Reativa de Props no Vue 3.5

A partir do Vue 3.5, as props podem ser desestruturadas diretamente do defineProps mantendo a reatividade. Isso elimina a necessidade de toRefs na maioria dos casos.

Fundamentos do Script Setup no Vue 3.5

A sintaxe <script setup> é um açúcar sintático em tempo de compilação para a Composition API dentro dos Single File Components. Variáveis declaradas no nível superior são automaticamente expostas ao template, e as importações são diretamente utilizáveis sem necessidade de registro.

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

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

// Estado reativo
const isExpanded = ref(false)

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

// Emissor de eventos com payload tipado
const emit = defineEmits<{
  select: [userId: string]
  delete: [userId: string]
}>()

// Métodos são funções 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 ? 'Recolher' : 'Expandir' }}
    </button>
  </div>
</template>

O compilador lida automaticamente com as declarações de exportação e o registro de componentes. Isso reduz a carga cognitiva em comparação com retornar explicitamente valores de uma função setup().

Desestruturação Reativa de Props (Vue 3.5+)

Antes do Vue 3.5, a desestruturação de props quebrava a reatividade porque a desestruturação do JavaScript cria uma cópia instantânea do valor naquele momento. O Vue 3.5 introduz um rastreamento em nível de compilador que preserva a reatividade para props desestruturadas.

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

// Desestruturação com valores padrão - permanece reativa no 3.5+
const { query = '', placeholder = 'Pesquisar...' } = defineProps<{
  query?: string
  placeholder?: string
}>()

// Watch funciona em props desestruturadas
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 sintaxe requer Vue 3.5 ou posterior. O anúncio do Vue 3.5 documenta a lista completa do comportamento de desestruturação reativa de props.

defineModel para Vinculação Bidirecional

Antes do defineModel, implementar v-model em um componente personalizado exigia declarar uma prop e emitir manualmente um evento de atualização. Este padrão aparece em inúmeras bases de código Vue:

vue
<!-- A forma antiga - verbosa mas 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>

Com defineModel, o mesmo componente se torna significativamente mais curto:

vue
<!-- TextInput.vue -->
<script setup lang="ts">
// defineModel cria uma ref que sincroniza com v-model
const model = defineModel<string>({ default: '' })
</script>

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

O componente pai usa a sintaxe padrão do 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 de usuário: {{ username }}</p>
</template>

defineModel retorna uma ref que permanece sincronizada com o valor vinculado do pai. As mutações desta ref emitem automaticamente o evento de atualização.

Pronto para mandar bem nas entrevistas de Vue.js / Nuxt.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Modelos Nomeados e Vinculações v-model Múltiplas

Os componentes às vezes precisam de múltiplas vinculações bidirecionais. O Vue suporta vinculações v-model nomeadas, e o defineModel as manipula aceitando um argumento de nome.

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

// Modelos nomeados para múltiplas vinculações v-model
const startDate = defineModel<Date>('start', { required: true })
const endDate = defineModel<Date>('end', { required: true })

// Validação 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>até</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">A data final deve ser posterior à data inicial</p>
  </div>
</template>

O pai vincula ambos os modelos:

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

Modificadores v-model com defineModel

Os modificadores nativos do Vue como .trim, .number e .lazy funcionam automaticamente com defineModel. Modificadores personalizados requerem tratamento explícito através do segundo elemento da tupla retornada.

vue
<!-- CurrencyInput.vue -->
<script setup lang="ts">
// Acesso aos modificadores via o segundo valor de retorno
const [model, modifiers] = defineModel<number>({
  default: 0,
  // Funções de transformação para modificadores personalizados
  set(value) {
    // Arredondar para 2 casas decimais se o modificador 'round' estiver presente
    if (modifiers.round) {
      return Math.round(value * 100) / 100
    }
    return value
  }
})
</script>

<template>
  <div class="currency-input">
    <span>R$</span>
    <input
      type="number"
      step="0.01"
      :value="model"
      @input="model = parseFloat(($event.target as HTMLInputElement).value) || 0"
    />
  </div>
</template>

Uso com o modificador personalizado:

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

Padrões TypeScript para Script Setup

A segurança de tipos em componentes <script setup> depende de genéricos passados às macros do compilador. O padrão varia ligeiramente entre props, emits e 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]
}>()

// Slots também podem ser tipados
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">Sem itens</slot>
  </div>
</template>

Componentes genéricos, disponíveis desde o Vue 3.3, permitem componentes tipados reutilizáveis sem sacrificar a inferência de tipos no código consumidor. Para padrões mais profundos, a documentação Vue sobre TypeScript cobre os casos de borda.

Perguntas de Entrevista Comuns sobre Script Setup

Os entrevistadores que testam conhecimento de Vue frequentemente focam nas diferenças entre script setup e Options API, bem como nos mecanismos de reatividade.

Pergunta: Por que a desestruturação de props quebra a reatividade, e como o Vue 3.5 resolve isso?

No JavaScript, a desestruturação cria novas variáveis contendo os valores no momento da desestruturação. Se props.count mudar depois, uma const { count } = props desestruturada ainda mantém o valor antigo. O compilador do Vue 3.5 transforma as variáveis desestruturadas em getters que são reavaliados quando acessados, preservando a reatividade sem sobrecarga em tempo de execução.

Pergunta: O que acontece internamente quando defineModel é usado?

O compilador expande defineModel em uma declaração de prop e um registro de emit. Em tempo de execução, ele cria uma ref cujo getter lê o valor da prop e cujo setter chama emit('update:modelValue', newValue). Esta ref não é uma cópia verdadeira da prop, mas um proxy que encaminha leituras e escritas apropriadamente.

Pergunta: Quando um componente deve usar script setup versus uma função setup regular?

Script setup é preferido para a maioria dos single-file components porque reduz o boilerplate e melhora o suporte do IDE. Uma função setup() regular é necessária quando o componente precisa expor métodos para componentes pais via defineExpose em padrões complexos, ou ao construir componentes renderless que retornam funções de render diretamente. Para preparação de entrevistas, explore mais padrões de composables Vue.

Migrando de Options API para Script Setup

As bases de código existentes frequentemente contêm componentes Options API que se beneficiam da migração. A conversão segue um padrão previsível:

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

A versão migrada é mais curta e a inferência de tipos funciona sem casts PropType. A equipe Vue fornece vue-codemod para migrações automatizadas, embora a revisão manual continue sendo necessária para componentes complexos.

Considerações de Performance no Script Setup

Componentes script setup compilam para código mais eficiente porque o compilador conhece a forma exata do componente em tempo de compilação. Especificamente:

  • As vinculações de template são resolvidas em tempo de compilação em vez de busca de propriedades em tempo de execução
  • Importações não utilizadas são eliminadas mais efetivamente pelo tree-shaking
  • A ausência de uma função envolvente reduz a sobrecarga de closures

Para aplicações que priorizam o tamanho do bundle, script setup com Vapor Mode (disponível no Vue 3.6+) elimina completamente o DOM virtual para os componentes que optam por ele, gerando operações DOM diretas em seu lugar.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Pontos-Chave para Vue 3 Script Setup e defineModel

  • Script setup reduz o boilerplate de componentes ao expor automaticamente as vinculações de nível superior para templates e manipular o registro de componentes
  • defineModel substitui o padrão manual prop + emit para v-model, retornando uma ref que sincroniza com o valor vinculado do pai
  • Vue 3.5 introduz a desestruturação reativa de props, permitindo const { prop } = defineProps() sem perder reatividade
  • Modelos nomeados suportam múltiplas vinculações v-model em um único componente usando defineModel('name')
  • Genéricos TypeScript funcionam com defineProps, defineEmits, defineSlots, e componentes genéricos via o atributo generic
  • A migração do Options API segue um padrão mecânico: data se torna refs, computed permanece como computed, métodos se tornam funções, e o compilador lida com os exports
Desafio do dia

Você saberia encontrar o bug em Vue.js / Nuxt.js?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 18 de setembro de 2026

Tags

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

Compartilhar

Artigos relacionados