Reputation: 17724
Consider the following code:
const router = useRouter()
await router.push({
name: 'NonExistingRoute', // no typescript error ):
})
A non existing route name was given, but no TypeScript error happens. Instead the issue will only be noticed on runtime. Any way to get a compile-time error on this?
Upvotes: 3
Views: 1333
Reputation: 9688
Coming from react and just fiddling around with Vue3 to understand it. I wanted to make routes more safe in regards to params and came across this old question. Just wanted to share my current take (just fiddling around as said):
import { createRouter, createWebHistory, type RouteLocationRaw } from 'vue-router'
import ChatRoomsView from '@/views/ChatRoomsView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'chat-rooms',
component: ChatRoomsView,
},
{
path: '/login',
name: 'login',
component: () => import('@/views/LoginView.vue')
},
{
path: '/chat-room/:id/',
name: 'chat-room',
component: () => import('@/views/ChatView.vue'),
},
{
// with hash mode, the part of the URL after the # symbol represents the "virtual" path of your SPA, and any changes to it won't trigger a server request. This makes it ideal for SPAs without server-side configurations
path: '/#/error/:message',
name: 'error',
component: () => import('@/views/ErrorView.vue')
}
]
})
const linking = {
login: {},
'chat-rooms': {},
'chat-room': { id: '' },
error: { message: '' }
};
// DEMO
router.push({ name: 'NonExistingRoute' } satisfies Record<'name', keyof Linking>)
function validateRoutes(routes: ReturnType<typeof router.getRoutes>, links: typeof linking) {
for (const route of routes) {
const expectedParams = links[route.name as keyof typeof linking];
const routeParams = (route.path.match(/:\w+/g) || []).map(p => p.slice(1));
const missingParams = Object.keys(expectedParams).filter(param => !routeParams.includes(param));
const undeclaredParams = routeParams.filter(param => !Object.keys(expectedParams).includes(param));
if (missingParams.length > 0 || undeclaredParams.length > 0) {
let errorMessage = `Route ${String(route.name)} has a mismatch in parameters.\nPath: ${route.path}\n`;
if (missingParams.length > 0) {
errorMessage += `Missing parameters: [${missingParams.join(', ')}]\n`;
}
if (undeclaredParams.length > 0) {
errorMessage += `Undeclared types for parameters: [${undeclaredParams.join(', ')}]`;
}
router.push({ name: 'error', params: { message: errorMessage.trim() } });
}
}
}
export const onMountValidateRoutes = () => validateRoutes(router.getRoutes(), linking);
export default router
export type RouteParamsExtractor<T, K extends keyof T> = T[K]
export type Linking = typeof linking
App.vue
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { onMountValidateRoutes } from './router'
onMountValidateRoutes()
</script>
<template>
<RouterView />
</template>
<style scoped></style>
ErrorView.vue
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const message = (route.params.message as string)
.split('\n')
.map((line) => line.trim())
.join('\n')
.trim()
</script>
<template>
<main>
<h1>Error</h1>
<pre>{{ message }}</pre>
</main>
</template>
<style scoped>
pre {
background-color: #1b202c;
color: var(--color-text);
padding: 10px;
border-radius: 5px;
font-size: 14px;
}
</style>
if I then haven typed (via the linking) the params I have declared in my routes I get to a screen that looks like:
Upvotes: 0
Reputation: 6917
my solution to this dilemma
define your routes in routes.ts
import { RouteLocationRaw } from 'vue-router'
type RouteFunction = (...args: any[]) => RouteLocationRaw
export const routes = {
login: () => ({ name: 'Login' }) as const,
viewProfile: (userId: string) => ({ name: 'ViewProfile', params: { userId } }) as const,
...
} satisfies Readonly<Record<string, RouteFunction>>
export type Routes = typeof routes
export type NamedRoute = ReturnType<Routes[keyof Routes]>['name']
Note that each route needs to be as const
, otherwise typescript will widen the ['name'] prop to any string
.
Then the actual router gets defined as
import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
import { NamedRoute } from '@/router/routes'
type NamedRouteRecordParent = { name?: NamedRoute, children: NamedRouteRecord[] }
type NamedRouteRecordChild = { name: NamedRoute }
type NamedRouteRecord = Omit<RouteRecordRaw, 'name' | 'children'> & NamedRouteRecordParent | NamedRouteRecordChild
const routes: NamedRouteRecord[] = [
{ name: 'Login', path: '/login' },
{ name: 'ViewProfile', path: '/view-profile/:userId' },
]
export const router = createRouter({
history: createWebHistory(),
routes: routes as RouteRecordRaw[],
})
which forces developers to only name routes that match records found in routes.ts
. As written, "parent" records do not have to be named.
Upvotes: 1
Reputation: 138
Perhaps you could wrap this in a utility function that only accepts typed route strings
const router = useRouter()
export type NamedRoute = "login" | "logout" | "user-profile";
export async function goToNamedRoute(name: NamedRoute): Promise<void> {
return router.push({name});
}
Upvotes: 1
Reputation: 100
If you want to rely on Typescript for detecting wrong routes you might just use enums or closed types maybe?, although that will surely require some composition. Probably one way to go could be:
enum Cities {
NY,
London
}
function routeCreator(city: Cities, restOfPath?: string){
//combine paths somehow, e.g.
if(!restOfPath) return `/${Cities[city]}/`;
return `/${Cities[city]}/${restOfPath}`
}
Upvotes: 0
Reputation: 17094
In short no.
For a compile error to exist there would need to be something explicitly wrong with the code, referencing an non-existent file, syntax error, etc.
It does sound like you are trying to solve some other issue here...i.e. why do you have the names of non-existing routes in your app?
In any case, perhaps you can avoid your errors programmatically, e.g.
let r = router.resolve({name: 'NonExistingRoute'});
if (r.resolved.matched.length > 0){
// exists
} else {
// doesn't exist
}
Upvotes: 0