
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: