- Artigos
- Desenvolvimento
- Padrões Avançados de TypeScript para Vue

O suporte a TypeScript no Vue 3 deixou de ser um extra opcional. Com script setup, composables tipados e inferência nativa para props e emits, dá para interceptar categorias inteiras de bugs antes que cheguem à produção. O desafio é saber quais padrões realmente escalam em aplicações reais — especialmente em projetos Nuxt, onde dados do servidor, composables compartilhados e bibliotecas de componentes se cruzam.
Este artigo cobre padrões avançados que vão além de defineProps<{ id: string }>(). Cada técnica resolve um problema concreto: componentes reutilizáveis que preservam informação de tipo, composables que funcionam com qualquer formato de API e máquinas de estado que tornam combinações impossíveis de UI simplesmente irrepresentáveis.
Componentes Genéricos que Preservam Tipos
Uma das frustrações mais comuns com componentes Vue é perder informação de tipo ao encapsular um conceito genérico — como tabela de dados, select ou lista assíncrona — em um shell reutilizável. Sem generics, você acaba fazendo cast de valores ou aceitando unknown em todo lugar.
O Vue 3.3+ suporta componentes genéricos em <script setup generic="T">. Isso permite declarar um parâmetro de tipo no próprio componente e propagá-lo por props, slots e emits:
<script setup lang="ts" generic="T extends { id: string }">
defineProps<{
items: T[]
selected?: T
}>()
defineEmits<{
select: [item: T]
}>()
</script>Quando o componente pai passa items como User[], o emit select e qualquer dado de slot com escopo inferem automaticamente User. Sem asserções manuais de tipo. Esse padrão é especialmente valioso em design systems onde um único componente de lista precisa atender dezenas de tipos de entidade.
Props, Emits e Slots Tipados em Conjunto
O Vue moderno incentiva o estilo de macros sem runtime:
const props = defineProps<{
modelValue: string
disabled?: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
submit: [payload: { value: string; timestamp: number }]
}>()Três detalhes separam tipagem de nível produção de boilerplate superficial:
- Use sintaxe de tupla nos emits. A forma
[value: string]habilita verificação estrita do payload do evento. Evite a sintaxe legada em objeto, a menos que mantenha código Vue 2. - Extraia tipos compartilhados para módulos. Defina
UserFormPayloaduma vez e importe tanto no componente quanto no composable que monta o payload. Tipos inline duplicados divergem silenciosamente. - Tipifique slots explicitamente quando carregam dados. Com
defineSlots<{ default(props: { item: T }): void }>(), quem consome ganha autocomplete dentro de scoped slots — um investimento pequeno que compensa em tabelas e tree views complexas.
Generics em Composables para Busca de Dados
Composables são onde o TypeScript brilha na arquitetura Vue. Um composable bem tipado funciona como contrato entre a UI e a camada de dados.
Considere um wrapper de fetch usado em um app Nuxt:
export function useAsyncResource<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const pending = ref(false)
async function execute() {
pending.value = true
try {
data.value = await $fetch<T>(toValue(url))
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
} finally {
pending.value = false
}
}
return { data: readonly(data), error: readonly(error), pending: readonly(pending), execute }
}O genérico T flui do ponto de chamada: useAsyncResource<User>('/api/users/1'). Combine isso com zod ou valibot na fronteira se as respostas da API não forem confiáveis. Faça o parse uma vez e trabalhe com o tipo inferido em todo o fluxo abaixo.
Para composables que aceitam objetos de configuração, use restrições extends em vez de generics sem limite. usePaginatedList<T extends Identifiable> garante que todo item tenha id, o que sua virtual scroll ou lógica de seleção pode assumir sem checagens de null.
Unions Discriminadas para Estado de Componente
Componentes frequentemente modelam estado com vários booleanos: loading, error, data. Isso convida combinações impossíveis — exibir spinner e mensagem de erro ao mesmo tempo, ou renderizar dados enquanto loading é true.
Substitua flags booleanas por uma union discriminada:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string }No template, faça switch em state.status. O TypeScript estreita o tipo em cada ramo, então state.data só fica acessível quando status === 'success'. Esse padrão funciona muito bem com propriedades computed e mantém condicionais do template honestas.
Estenda a mesma ideia para wizards de formulário, fluxos de checkout e modais em que cada etapa tem ações disponíveis diferentes. A union documenta a máquina de estados nos tipos, não em comentários.
Provide e Inject com Segurança de Tipos
Provide/inject é poderoso para injeção de dependência em árvores profundamente aninhadas, mas chaves de injeção sem tipo são fonte comum de erros de undefined em runtime.
Defina uma InjectionKey com tipo explícito:
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
mode: Ref<'light' | 'dark'>
toggle: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')No provider: provide(ThemeKey, { mode, toggle }). Nos consumidores: const theme = inject(ThemeKey) — totalmente tipado, sem adivinhação de fallback. Para injeção opcional, use inject(ThemeKey, null) e trate o caso null explicitamente, em vez de injetar silenciosamente um objeto dummy.
Template Refs e Instâncias de Componente Tipadas
Template refs no Vue 3 exigem tipagem deliberada. Para elementos DOM:
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})Para instâncias de componentes filhos, use InstanceType<typeof ChildComponent> ou importe a interface exposta se usar defineExpose:
const editorRef = ref<InstanceType<typeof RichTextEditor> | null>(null)
function save() {
editorRef.value?.getContent()
}Exponha apenas o que os pais precisam. Uma superfície pública estreita é mais fácil de tipar e menos frágil do que expor todo o interior do componente.
Stores Pinia com Tipos de Retorno Inferidos
Setup stores do Pinia espelham composables, o que significa excelente inferência quando você retorna objetos simples. Defina o estado da store com interfaces explícitas para entidades, mas deixe as actions inferirem o tipo de retorno, a menos que precise de um contrato estável para testes.
Quando stores dependem umas das outras, importe composables de store dentro das actions em vez do topo do módulo, para evitar dependências circulares. Tipifique chamadas entre stores pelas assinaturas das actions retornadas — o Pinia as preserva automaticamente.
Para SSR no Nuxt, lembre que o estado da store precisa ser serializável. Tipifique fatias persistidas separadamente de estado efêmero de UI, como flags de painel aberto/fechado. Uma UiStore e uma CartStore com regras de persistência diferentes mantêm tipos e lógica de hidratação limpos.
Utility Types para Camadas de API e Formulário
Apps Vue gastam esforço significativo mapeando DTOs de API para view models. Utility types padrão do TypeScript reduzem duplicação:
- Pick e Omit para inputs de formulário que aceitam atualizações parciais de entidade
- Partial<T> para estado de rascunho antes da validação passar
- Record<K, V> para dicionários indexados renderizados como listas de opções
- Extract e Exclude para estreitar props union em componentes wrapper
Construa helpers pequenos específicos do domínio: type CreateInput<T> = Omit<T, 'id' | 'createdAt'>. Use-os de forma consistente em composables, stores e schemas de formulário para que refactors se propaguem pelo compilador.
Juntando Tudo em um Projeto Nuxt
No Nuxt 3, coloque tipos em ~/types ou junto às features em estrutura modular. Composables auto-importados se beneficiam de anotações explícitas de retorno apenas quando a inferência falha — geralmente em fronteiras genéricas ou ao retornar formatos condicionais.
Ative modo strict no tsconfig.json: strict: true, noUncheckedIndexedAccess: true, e considere exactOptionalPropertyTypes para modelos de API. O compilador do Vue e o Volar vão surfacear a maioria dos erros de tipo no template em tempo de edição.
TypeScript avançado no Vue não é sobre maximizar densidade de anotações. É sobre escolher padrões — generics, unions, injection keys, emits tipados — que codificam os invariantes da sua aplicação para o compilador funcionar como parceiro de design. Comece por um ponto de dor: uma lista genérica com vazamento de tipo, um composable sem tipagem ou uma máquina de estados cheia de booleanos. Refatore usando os padrões acima, e o restante do codebase tende a seguir naturalmente.