Skip to main content
Advertisement

12.5 Vue Router 4 + TypeScript — Route Types and Navigation Guards

Vue Router 4 Basic Setup

npm install vue-router@4
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/users',
name: 'users',
component: () => import('@/views/UsersView.vue'),
},
{
path: '/users/:id',
name: 'user-detail',
component: () => import('@/views/UserDetailView.vue'),
props: true, // Pass params as props
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: {
requiresAuth: true,
roles: ['admin', 'user'],
},
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFoundView.vue'),
},
]

const router = createRouter({
history: createWebHistory(),
routes,
})

export default router

Extending Route Meta Types

// router/types.ts
import 'vue-router'

// Extend route meta types
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: ('admin' | 'user' | 'guest')[]
title?: string
breadcrumb?: string
keepAlive?: boolean
}
}

useRoute and useRouter Types

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()
const router = useRouter()

// route.params: RouteParamsRaw
const userId = computed(() => route.params.id as string)

// route.query: LocationQuery
const page = computed(() => {
const p = route.query.page
return typeof p === 'string' ? parseInt(p) : 1
})

// Programmatic navigation
function goToUser(id: string) {
router.push({ name: 'user-detail', params: { id } })
}

function goBack() {
router.back()
}

function goWithQuery() {
router.push({
name: 'users',
query: { page: '2', sort: 'name' },
})
}
</script>

Type-Safe Routes — TypedRouteMap

Vue Router 4.4+ supports type-safe route configuration.

// router/typed-routes.ts
import {
createRouter,
createWebHistory,
RouteRecordRaw,
} from 'vue-router'

// Define params/query types per route
interface TypedRoutes {
home: {
params: {}
query: {}
}
userDetail: {
params: { id: string }
query: { tab?: string }
}
search: {
params: {}
query: { q: string; page?: string; category?: string }
}
}

Global Guards

// router/guards.ts
import { Router, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'

export function setupGuards(router: Router) {
// Global beforeEach
router.beforeEach(async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
// Authentication check
if (to.meta.requiresAuth) {
const userStore = useUserStore()

if (!userStore.isLoggedIn) {
return next({
name: 'login',
query: { redirect: to.fullPath },
})
}

// Permission check
if (to.meta.roles && !to.meta.roles.includes(userStore.currentUser!.role)) {
return next({ name: 'forbidden' })
}
}

// Set page title
if (to.meta.title) {
document.title = `${to.meta.title} | My App`
}

next()
})

// Global afterEach
router.afterEach((to, from) => {
// Page tracking
analytics.pageView(to.fullPath)
})
}

In-Component Guards

<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// Hook called before route changes
onBeforeRouteUpdate(async (to, from) => {
// Same component, only params changed (e.g., /users/1 → /users/2)
const newId = to.params.id as string
await loadUser(newId)
})

// Hook before leaving the route (warn about unsaved data)
onBeforeRouteLeave((to, from, next) => {
if (hasUnsavedChanges.value) {
const confirmed = window.confirm('You have unsaved changes. Are you sure you want to leave?')
if (confirmed) {
next()
} else {
next(false) // Cancel navigation
}
} else {
next()
}
})
</script>

<template>
<!-- Basic RouterLink -->
<RouterLink to="/home">Home</RouterLink>

<!-- Named route -->
<RouterLink :to="{ name: 'user-detail', params: { id: userId } }">
User Details
</RouterLink>

<!-- With query -->
<RouterLink :to="{ name: 'search', query: { q: searchQuery, page: '1' } }">
Search
</RouterLink>
</template>

Dynamic Route Addition

// Dynamically register routes based on permissions
import { router } from '@/router'

function addAdminRoutes() {
router.addRoute({
path: '/admin',
name: 'admin',
component: () => import('@/views/AdminView.vue'),
children: [
{
path: 'users',
name: 'admin-users',
component: () => import('@/views/admin/UsersManagement.vue'),
},
],
})
}

// Add routes after user login
const userStore = useUserStore()
watch(() => userStore.isAdmin, (isAdmin) => {
if (isAdmin) addAdminRoutes()
})

Pro Tips

1. Custom RouterLink with useLink

<script setup lang="ts">
import { useLink, RouterLinkProps } from 'vue-router'

const props = defineProps<RouterLinkProps>()
const { isActive, isExactActive, href, navigate } = useLink(props)
</script>

<template>
<a
:href="href"
:class="{ 'active': isActive, 'exact-active': isExactActive }"
@click.prevent="navigate"
>
<slot />
</a>
</template>

2. Auto-generate component types per route (unplugin-vue-router)

npm install -D unplugin-vue-router

Supports file-based routing with automatic type generation. Enables type-safe params access using route names like useRoute('user-detail').

Advertisement