⚡ Gabriel Caiana
imagem principal do site

Nuxt 4: TypeScript e Type Safety - Guia Completo


O Nuxt 4 eleva o suporte ao TypeScript para um novo patamar, oferecendo type safety completo e nativo em toda a aplicação. Neste guia, vamos explorar como o Nuxt 4 gera tipos automaticamente, melhora a inferência e proporciona uma experiência de desenvolvimento TypeScript excepcional.

🧭 Guia de Navegação Rápida

🏷️ Type Safety Básico

🔧 Configuração e Setup

💡 Exemplos Práticos


🚀 Auto-generated Types

Tipos Automáticos para Pages

O Nuxt 4 gera automaticamente tipos para todas as suas páginas, rotas e parâmetros.

// .nuxt/types/app.d.ts (gerado automaticamente)
declare global {
  // ✨ Types para pages automáticos
  interface NuxtRoutes {
    '/': {}
    '/about': {}
    '/blog': {}
    '/blog/[slug]': { slug: string }
    '/products': {}
    '/products/[id]': { id: string }
    '/user/profile': {}
    '/admin/dashboard': {}
    '/checkout/[step]': { step: 'shipping' | 'payment' | 'confirmation' }
  }

  // ✨ Types para server API
  interface NuxtServerRoutes {
    '/api/products': {
      GET: { 
        query: { 
          category?: string; 
          page?: number 
        }; 
        response: Product[] 
      }
      POST: { 
        body: CreateProductDto; 
        response: Product 
      }
    }
    '/api/users/[id]': {
      GET: { 
        params: { id: string }; 
        response: User 
      }
      PUT: { 
        params: { id: string }; 
        body: UpdateUserDto; 
        response: User 
      }
      DELETE: { 
        params: { id: string }; 
        response: { success: boolean } 
      }
    }
  }

  // ✨ Types para composables
  interface NuxtComposables {
    useAuth: () => {
      user: Ref<User | null>
      isAuthenticated: ComputedRef<boolean>
      login: (credentials: LoginCredentials) => Promise<void>
      logout: () => void
    }
    useProducts: () => {
      products: Ref<Product[]>
      loading: Ref<boolean>
      fetchProducts: () => Promise<void>
    }
  }
}

Tipos para Configuração da Aplicação

// .nuxt/types/app.d.ts
declare global {
  interface NuxtCustomAppConfig {
    // ✨ Configuração de tema
    theme: {
      primaryColor: string
      secondaryColor: string
      darkMode: boolean
      borderRadius: 'sm' | 'md' | 'lg'
    }
    
    // ✨ Configuração de API
    api: {
      baseUrl: string
      timeout: number
      retries: number
      headers: Record<string, string>
    }
    
    // ✨ Configuração de features
    features: {
      analytics: boolean
      notifications: boolean
      offline: boolean
      pwa: boolean
    }
  }

  // ✨ Types para runtime config
  interface RuntimeConfig {
    public: {
      apiBase: string
      appVersion: string
      environment: 'development' | 'staging' | 'production'
    }
    private: {
      databaseUrl: string
      jwtSecret: string
      stripeKey: string
    }
  }
}

🎯 Type Safety Aprimorado

Type Safety em Composables

// app/composables/useAuth.ts
interface User {
  id: string
  email: string
  name: string
  role: 'user' | 'admin' | 'moderator'
  avatar?: string
  preferences: {
    theme: 'light' | 'dark'
    language: 'pt-BR' | 'en-US' | 'es-ES'
    notifications: boolean
  }
}

interface LoginCredentials {
  email: string
  password: string
  rememberMe?: boolean
}

interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  isLoading: boolean
}

export const useAuth = () => {
  // ✨ State tipado com useState
  const user = useState<User | null>('auth:user', () => null)
  const token = useState<string | null>('auth:token', () => null)
  const isLoading = useState<boolean>('auth:loading', () => false)
  
  // ✨ Computed properties com type safety
  const isAuthenticated = computed<boolean>(() => !!user.value && !!token.value)
  const userRole = computed<'user' | 'admin' | 'moderator' | null>(() => user.value?.role || null)
  const canAccessAdmin = computed<boolean>(() => userRole.value === 'admin')
  
  // ✨ Funções com tipos explícitos
  const login = async (credentials: LoginCredentials): Promise<void> => {
    try {
      isLoading.value = true
      
      const response = await $fetch<{ user: User; token: string }>('/api/auth/login', {
        method: 'POST',
        body: credentials
      })
      
      user.value = response.user
      token.value = response.token
      
      // Salvar no localStorage se rememberMe for true
      if (credentials.rememberMe) {
        localStorage.setItem('auth:token', response.token)
      }
    } catch (error) {
      throw new Error(`Login failed: ${error.message}`)
    } finally {
      isLoading.value = false
    }
  }
  
  const logout = (): void => {
    user.value = null
    token.value = null
    localStorage.removeItem('auth:token')
  }
  
  const updateProfile = async (updates: Partial<User>): Promise<User> => {
    if (!user.value) {
      throw new Error('User not authenticated')
    }
    
    const updatedUser = await $fetch<User>(`/api/users/${user.value.id}`, {
      method: 'PUT',
      body: updates
    })
    
    user.value = updatedUser
    return updatedUser
  }
  
  return {
    // ✨ State
    user: readonly(user),
    token: readonly(token),
    isLoading: readonly(isLoading),
    
    // ✨ Computed
    isAuthenticated,
    userRole,
    canAccessAdmin,
    
    // ✨ Actions
    login,
    logout,
    updateProfile
  }
}

Type Safety em Componentes

// app/components/UserProfile.vue
<template>
  <div class="user-profile">
    <div v-if="user" class="profile-content">
      <img :src="user.avatar || '/default-avatar.png'" :alt="user.name" />
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      <span class="role-badge">{{ user.role }}</span>
      
      <div class="preferences">
        <h3>Preferências</h3>
        <select v-model="user.preferences.theme">
          <option value="light">Tema Claro</option>
          <option value="dark">Tema Escuro</option>
        </select>
        
        <select v-model="user.preferences.language">
          <option value="pt-BR">Português</option>
          <option value="en-US">English</option>
          <option value="es-ES">Español</option>
        </select>
        
        <label>
          <input 
            type="checkbox" 
            v-model="user.preferences.notifications" 
          />
          Notificações
        </label>
      </div>
      
      <button @click="savePreferences" :disabled="saving">
        {{ saving ? 'Salvando...' : 'Salvar Preferências' }}
      </button>
    </div>
    
    <div v-else class="loading">
      Carregando perfil...
    </div>
  </div>
</template>

<script setup lang="ts">
// ✨ Props tipados
interface Props {
  userId?: string
  showPreferences?: boolean
  editable?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showPreferences: true,
  editable: true
})

// ✨ Emits tipados
interface Emits {
  profileUpdated: [user: User]
  preferencesChanged: [preferences: User['preferences']]
}

const emit = defineEmits<Emits>()

// ✨ Composables com type safety
const { user, updateProfile } = useAuth()
const { $toast } = useNuxtApp()

// ✨ State local tipado
const saving = ref<boolean>(false)
const originalPreferences = ref<User['preferences'] | null>(null)

// ✨ Computed com type safety
const canEdit = computed<boolean>(() => props.editable && !!user.value)
const hasChanges = computed<boolean>(() => {
  if (!user.value || !originalPreferences.value) return false
  
  return JSON.stringify(user.value.preferences) !== JSON.stringify(originalPreferences.value)
})

// ✨ Lifecycle com types
onMounted(() => {
  if (user.value) {
    originalPreferences.value = { ...user.value.preferences }
  }
})

// ✨ Watch com type safety
watch(() => user.value?.preferences, (newPrefs) => {
  if (newPrefs && originalPreferences.value) {
    const hasChanges = JSON.stringify(newPrefs) !== JSON.stringify(originalPreferences.value)
    if (hasChanges) {
      emit('preferencesChanged', newPrefs)
    }
  }
}, { deep: true })

// ✨ Funções com tipos explícitos
const savePreferences = async (): Promise<void> => {
  if (!user.value) return
  
  try {
    saving.value = true
    
    const updatedUser = await updateProfile({
      preferences: user.value.preferences
    })
    
    originalPreferences.value = { ...updatedUser.preferences }
    emit('profileUpdated', updatedUser)
    
    $toast.success('Preferências salvas com sucesso!')
  } catch (error) {
    $toast.error('Erro ao salvar preferências')
    console.error('Error saving preferences:', error)
  } finally {
    saving.value = false
  }
}

// ✨ Expose com types
defineExpose<{
  savePreferences: () => Promise<void>
  hasChanges: boolean
}>({
  savePreferences,
  hasChanges: hasChanges.value
})
</script>

⚙️ Configuração TypeScript

Configuração Básica

// tsconfig.json
{
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    
    // ✨ Configurações específicas do Nuxt 4
    "types": ["@nuxt/types", "@nuxt/kit"],
    "paths": {
      "~/*": ["./app/*"],
      "@/*": ["./app/*"],
      "shared/*": ["./shared/*"]
    }
  },
  
  "include": [
    "app/**/*",
    "shared/**/*",
    "server/**/*",
    ".nuxt/**/*"
  ],
  
  "exclude": [
    "node_modules",
    ".nuxt",
    "dist"
  ]
}

Configuração do Nuxt

// nuxt.config.ts
export default defineNuxtConfig({
  // 🆕 Configurações TypeScript
  typescript: {
    strict: true,
    typeCheck: true,
    shim: false,
    
    // 🆕 Configurações avançadas
    tsConfig: {
      compilerOptions: {
        strictNullChecks: true,
        noImplicitAny: true,
        noUnusedLocals: true
      }
    }
  },
  
  // 🆕 Auto-imports com types
  imports: {
    dirs: [
      'app/composables',
      'app/utils',
      'shared/types',
      'shared/utils'
    ],
    
    // 🆕 Global types
    global: true
  },
  
  // 🆕 DevTools para debugging de types
  devtools: {
    enabled: true
  }
})

🔧 Type Declarations

Declarações de Tipos Customizadas

// shared/types/index.ts
export interface BaseEntity {
  id: string
  createdAt: Date
  updatedAt: Date
}

export interface User extends BaseEntity {
  email: string
  name: string
  role: UserRole
  avatar?: string
  preferences: UserPreferences
  metadata: Record<string, any>
}

export type UserRole = 'user' | 'admin' | 'moderator'

export interface UserPreferences {
  theme: 'light' | 'dark' | 'auto'
  language: 'pt-BR' | 'en-US' | 'es-ES'
  notifications: {
    email: boolean
    push: boolean
    sms: boolean
  }
  privacy: {
    profileVisible: boolean
    showEmail: boolean
    showActivity: boolean
  }
}

// ✨ Utility types
export type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
export type UpdateUserDto = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
export type UserWithoutSensitiveData = Omit<User, 'email' | 'metadata'>

// ✨ API response types
export interface ApiResponse<T> {
  data: T
  meta: {
    page?: number
    totalPages?: number
    total?: number
    timestamp: string
  }
  success: boolean
  message?: string
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  meta: {
    page: number
    totalPages: number
    total: number
    limit: number
    timestamp: string
  }
}

// ✨ Error types
export interface ApiError {
  code: string
  message: string
  details?: Record<string, any>
  timestamp: string
}

// ✨ Event types
export interface UserEvent {
  type: 'login' | 'logout' | 'profile_update' | 'preferences_change'
  userId: string
  timestamp: Date
  metadata?: Record<string, any>
}

Declarações de Módulos

// shared/types/modules.d.ts
declare module 'vue-toastification' {
  interface ToastOptions {
    position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'
    timeout?: number
    closeOnClick?: boolean
    pauseOnFocusLoss?: boolean
    pauseOnHover?: boolean
    draggable?: boolean
    draggablePercent?: number
    showCloseButtonOnHover?: boolean
    hideProgressBar?: boolean
    closeButton?: boolean | string
    icon?: boolean | string
    rtl?: boolean
  }
  
  interface ToastInterface {
    (message: string, options?: ToastOptions): string
    success(message: string, options?: ToastOptions): string
    error(message: string, options?: ToastOptions): string
    warning(message: string, options?: ToastOptions): string
    info(message: string, options?: ToastOptions): string
    clear(): void
    dismiss(id: string): void
  }
  
  const toast: ToastInterface
  export default toast
}

declare module '@nuxt/schema' {
  interface NuxtConfig {
    myCustomModule?: {
      enabled: boolean
      apiKey?: string
      options?: Record<string, any>
    }
  }
}

🎯 Generics e Type Utilities

Composables Genéricos

// app/composables/useApi.ts
export const useApi = <T extends BaseEntity>() => {
  // ✨ Generic CRUD operations
  const create = async (data: CreateEntityDto<T>): Promise<T> => {
    return await $fetch<T>(`/api/${getEntityEndpoint<T>()}`, {
      method: 'POST',
      body: data
    })
  }
  
  const getById = async (id: string): Promise<T> => {
    return await $fetch<T>(`/api/${getEntityEndpoint<T>()}/${id}`)
  }
  
  const getAll = async (params?: QueryParams): Promise<PaginatedResponse<T>> => {
    return await $fetch<PaginatedResponse<T>>(`/api/${getEntityEndpoint<T>()}`, {
      query: params
    })
  }
  
  const update = async (id: string, data: UpdateEntityDto<T>): Promise<T> => {
    return await $fetch<T>(`/api/${getEntityEndpoint<T>()}/${id}`, {
      method: 'PUT',
      body: data
    })
  }
  
  const remove = async (id: string): Promise<{ success: boolean }> => {
    return await $fetch<{ success: boolean }>(`/api/${getEntityEndpoint<T>()}/${id}`, {
      method: 'DELETE'
    })
  }
  
  return {
    create,
    getById,
    getAll,
    update,
    remove
  }
}

// ✨ Type utilities para inferir tipos
type CreateEntityDto<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>
type UpdateEntityDto<T> = Partial<CreateEntityDto<T>>

// ✨ Helper para obter endpoint baseado no tipo
function getEntityEndpoint<T>(): string {
  const entityName = ({} as T).constructor.name.toLowerCase()
  return entityName.replace(/entity$/, '') + 's'
}

// ✨ Uso com tipos específicos
export const useUsers = () => useApi<User>()
export const useProducts = () => useApi<Product>()
export const useOrders = () => useApi<Order>()

Type Guards e Validação

// shared/utils/typeGuards.ts
export const isUser = (obj: any): obj is User => {
  return obj && 
    typeof obj.id === 'string' &&
    typeof obj.email === 'string' &&
    typeof obj.name === 'string' &&
    ['user', 'admin', 'moderator'].includes(obj.role)
}

export const isProduct = (obj: any): obj is Product => {
  return obj &&
    typeof obj.id === 'string' &&
    typeof obj.name === 'string' &&
    typeof obj.price === 'number' &&
    typeof obj.category === 'string'
}

export const isApiResponse = <T>(obj: any): obj is ApiResponse<T> => {
  return obj &&
    typeof obj.success === 'boolean' &&
    obj.data !== undefined &&
    obj.meta &&
    typeof obj.meta.timestamp === 'string'
}

// ✨ Type-safe validation
export const validateUser = (data: unknown): User => {
  if (isUser(data)) {
    return data
  }
  throw new Error('Invalid user data')
}

export const validateProduct = (data: unknown): Product => {
  if (isProduct(data)) {
    return data
  }
  throw new Error('Invalid product data')
}

💡 Exemplos de Uso

Exemplo de Formulário Tipado

// app/components/UserForm.vue
<template>
  <form @submit.prevent="handleSubmit" class="user-form">
    <div class="form-group">
      <label for="name">Nome</label>
      <input 
        id="name"
        v-model="form.name"
        type="text"
        required
        :class="{ error: errors.name }"
      />
      <span v-if="errors.name" class="error-message">{{ errors.name }}</span>
    </div>
    
    <div class="form-group">
      <label for="email">Email</label>
      <input 
        id="email"
        v-model="form.email"
        type="email"
        required
        :class="{ error: errors.email }"
      />
      <span v-if="errors.email" class="error-message">{{ errors.email }}</span>
    </div>
    
    <div class="form-group">
      <label for="role">Função</label>
      <select id="role" v-model="form.role">
        <option value="user">Usuário</option>
        <option value="moderator">Moderador</option>
        <option value="admin">Administrador</option>
      </select>
    </div>
    
    <button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? 'Salvando...' : 'Salvar Usuário' }}
    </button>
  </form>
</template>

<script setup lang="ts">
// ✨ Props tipados
interface Props {
  user?: User
  mode: 'create' | 'edit'
}

const props = defineProps<Props>()

// ✨ Emits tipados
interface Emits {
  saved: [user: User]
  cancelled: []
}

const emit = defineEmits<Emits>()

// ✨ Form state tipado
interface FormData {
  name: string
  email: string
  role: UserRole
}

interface FormErrors {
  name?: string
  email?: string
}

const form = reactive<FormData>({
  name: props.user?.name || '',
  email: props.user?.email || '',
  role: props.user?.role || 'user'
})

const errors = reactive<FormErrors>({})
const isSubmitting = ref<boolean>(false)

// ✨ Validation com types
const validateForm = (): boolean => {
  errors.name = undefined
  errors.email = undefined
  
  if (!form.name.trim()) {
    errors.name = 'Nome é obrigatório'
  }
  
  if (!form.email.trim()) {
    errors.email = 'Email é obrigatório'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
    errors.email = 'Email inválido'
  }
  
  return !errors.name && !errors.email
}

// ✨ Submit handler tipado
const handleSubmit = async (): Promise<void> => {
  if (!validateForm()) return
  
  try {
    isSubmitting.value = true
    
    let user: User
    
    if (props.mode === 'create') {
      user = await $fetch<User>('/api/users', {
        method: 'POST',
        body: form
      })
    } else {
      if (!props.user?.id) throw new Error('User ID required for edit mode')
      
      user = await $fetch<User>(`/api/users/${props.user.id}`, {
        method: 'PUT',
        body: form
      })
    }
    
    emit('saved', user)
  } catch (error) {
    console.error('Error saving user:', error)
    // Handle error appropriately
  } finally {
    isSubmitting.value = false
  }
}

// ✨ Watch para validação em tempo real
watch(() => form.name, () => {
  if (errors.name) {
    errors.name = undefined
  }
})

watch(() => form.email, () => {
  if (errors.email) {
    errors.email = undefined
  }
})
</script>

🎯 Melhores Práticas

1. Organização de Tipos

// shared/types/index.ts - Arquivo principal
export * from './entities'
export * from './api'
export * from './events'
export * from './utils'

// shared/types/entities.ts - Entidades do domínio
export interface User { /* ... */ }
export interface Product { /* ... */ }

// shared/types/api.ts - Tipos de API
export interface ApiResponse<T> { /* ... */ }
export interface PaginatedResponse<T> { /* ... */ }

// shared/types/events.ts - Eventos da aplicação
export interface UserEvent { /* ... */ }
export interface ProductEvent { /* ... */ }

// shared/types/utils.ts - Utility types
export type CreateDto<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>
export type UpdateDto<T> = Partial<CreateDto<T>>

2. Type Safety em Composables

// app/composables/useTypedState.ts
export const useTypedState = <T>(key: string, defaultValue: T) => {
  const state = useState<T>(key, () => defaultValue)
  
  const setState = (value: T | ((current: T) => T)) => {
    if (typeof value === 'function') {
      state.value = (value as (current: T) => T)(state.value)
    } else {
      state.value = value
    }
  }
  
  const resetState = () => {
    state.value = defaultValue
  }
  
  return {
    state: readonly(state),
    setState,
    resetState
  }
}

// ✨ Uso tipado
const { state: user, setState: setUser, resetState: resetUser } = useTypedState<User | null>('user', null)

3. Error Handling Tipado

// shared/utils/errorHandling.ts
export class TypedError extends Error {
  constructor(
    message: string,
    public code: string,
    public details?: Record<string, any>
  ) {
    super(message)
    this.name = 'TypedError'
  }
}

export const handleApiError = (error: unknown): TypedError => {
  if (error instanceof TypedError) {
    return error
  }
  
  if (error instanceof Error) {
    return new TypedError(error.message, 'UNKNOWN_ERROR')
  }
  
  return new TypedError('An unknown error occurred', 'UNKNOWN_ERROR')
}

// ✨ Uso em composables
export const useSafeApi = <T>() => {
  const execute = async (operation: () => Promise<T>): Promise<{ data: T } | { error: TypedError }> => {
    try {
      const data = await operation()
      return { data }
    } catch (error) {
      const typedError = handleApiError(error)
      return { error: typedError }
    }
  }
  
  return { execute }
}

✨ Conclusão

O sistema de TypeScript do Nuxt 4 oferece:

  • Type Safety Completo: Em toda a aplicação, desde componentes até APIs
  • Auto-generated Types: Tipos automáticos para rotas, APIs e composables
  • Inferência Inteligente: TypeScript infere tipos automaticamente
  • Developer Experience: IntelliSense completo e detecção de erros em tempo real
  • Performance: Zero overhead em runtime, apenas em desenvolvimento

Com essas ferramentas, você pode criar aplicações Vue.js com a mesma segurança de tipos de linguagens como Rust ou Haskell, proporcionando uma base sólida para projetos de qualquer tamanho.


📚 Artigos Relacionados:

🔗 Recursos Oficiais: