In Vue Router 5 file routing, how does the generated RouteNamedMap type-check router.push and useRoute(), and which mistakes does it still let through?
answer
- a generated declaration file
- TypesConfig augmentation
- raw params versus normalized params
- string paths are only suggestions
- useRoute(name) is a type assertion
basics
~20 sThe plugin writes a declaration file whose RouteNamedMap lists each route's name, path and params and plugs it into Vue Router's TypesConfig. Named pushes then check names and params, but string paths, runtime-added routes and a stale file slip through.
solid answer
~40 sWith TypeScript installed, the Vue Router 5 plugin writes a declaration file (by default `typed-router.d.ts`, recommended `src/route-map.d.ts`) with a `RouteNamedMap` of `RouteRecordInfo` entries and augments `TypesConfig` with it. From then on `router.push({ name: '/posts/[slug]', params: { slug } })` checks that the name exists and that params match: raw params accept `string | number`, while `route.params.slug` reads back as `string`. `useRoute('/posts/[slug]')` narrows the route type to that page and its children. The gaps: a string path like `router.push('/post/' + slug)` accepts any string, `useRoute(name)` is never checked at runtime, routes pushed onto the array at runtime have no types, and an uncommitted or stale file lies until the plugin runs again.
code
vue · 15 lines<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute('/posts/[slug]')
const router = useRouter()
function openRelated(slug: string) {
// checked: name exists, slug is present
router.push({ name: '/posts/[slug]', params: { slug } })
}
</script>
<template>
<h1>{{ route.params.slug }}</h1>
</template>go deeper
Recall that the plugin generates a types file, so misspelled route names in named pushes become compile errors.
Explain RouteRecordInfo: name, path, raw params accepted by push, normalized params read from the route, and the TypesConfig augmentation that activates them.
Show where the safety ends: string paths, the type-only useRoute(name), runtime-added routes and a stale generated file, and how you close each gap in CI.
Argue for named locations as a team rule and a committed generated map, so a route rename becomes a compiler-listed change rather than a runtime surprise.
## Where the types come from Typed routes have existed in Vue Router since 4.4 as a **manual** option: you write an interface of routes and augment the router's `TypesConfig`. In Vue Router 5 the file-based routing plugin generates that for you. When TypeScript is installed (`dts` defaults to on in that case) it writes a declaration file that: - declares, in the `vue-router/auto-routes` module, a `RouteNamedMap` interface with one entry per named route; - augments `TypesConfig` in `vue-router` with `RouteNamedMap`, which switches the router's public types from generic to typed. Each entry is a `RouteRecordInfo<Name, Path, ParamsRaw, Params, ChildrenNames>`. For a blog post page: | Slot | Value for `posts/[slug].vue` | Meaning | |---|---|---| | `Name` | `'/posts/[slug]'` | the route name, derived from the file | | `Path` | `'/posts/:slug'` | the path, offered in autocompletion | | `ParamsRaw` | `{ slug: string \| number }` | what `push` and `RouterLink` accept | | `Params` | `{ slug: string }` | what the current route exposes | | `ChildrenNames` | `never` | names of nested child routes | The file is written to the project root as `typed-router.d.ts` by default; the Vue Router 5 migration guide recommends `dts: 'src/route-map.d.ts'`, since `src/` is included by most `tsconfig` setups. ## What becomes checked ```ts const router = useRouter() router.push({ name: '/posts/[slug]', params: { slug: 'hello' } }) // ok router.push({ name: '/posts/[slug]', params: { slug: 42 } }) // ok: raw accepts numbers router.push({ name: '/posts/[slg]', params: { slug: 'x' } }) // error: unknown name router.push({ name: '/posts/[slug]', params: {} }) // error: slug missing router.push({ name: '/posts/[slug]' }) // ok: reuses current params router.push('/post/' + 'hello') // ok: any string compiles ``` - **Names** in named locations, `RouterLink` `to` objects and guard comparisons such as `to.name === '/posts/[slug]'` are checked against the map. - **Params** are checked per route: a required param cannot be left out of a `params` object, and a repeatable param must be an array. - **`useRoute('/posts/[slug]')`** returns a route typed for that name and its children, so `route.params.slug` is a `string`. The `vue-router/volar/sfc-typed-router` Volar plugin goes further and types a bare `useRoute()` from the page file it is called in. ## What still gets through 1. **String paths.** A string location is typed as the known paths *or any string*: autocompletion helps, but `router.push('/post/' + slug)` with a typo compiles. 2. **The `useRoute(name)` argument is type-only.** The implementation ignores it; if a shared component calls `useRoute('/posts/[slug]')` while rendered on another route, the types promise a `slug` that is not there. 3. **Runtime edits.** Records pushed onto `routes` before `createRouter()`, such as legacy redirects, work at runtime but are absent from the map. 4. **A stale or missing file.** The declaration is written when the plugin runs, during the dev server or a build. A type-check that runs first sees an old map. The generated header recommends committing the file, so reviews and CI see the same map. 5. **Omitted params.** `router.push({ name })` without a `params` object compiles, because the router can reuse the current params; whether that is right depends on where you navigate from. ## Making the editor and the checker see it - **Include the file.** The generated header says to add it to `tsconfig.json` as an `include` or `files` entry; placing it in `src/` with `dts: 'src/route-map.d.ts'` usually makes that automatic. - **Resolution mode.** The getting-started guide pairs the file with `"moduleResolution": "Bundler"` in `compilerOptions`. - **Volar plugins.** Adding `vue-router/volar/sfc-typed-router` and `vue-router/volar/sfc-route-blocks` to `vueCompilerOptions.plugins` types a bare `useRoute()` per page and enables typed `<route>` blocks. - **When types seem missing,** the troubleshooting advice is to restart the TypeScript server and confirm the generated file is included. ## Operating it well - Prefer **named locations** over string paths in application code; they are the only form whose params are checked. - Commit the generated file and point `dts` into `src/`, so editor, CI type-check and reviewers share one map. - Keep runtime-only routes to redirects and similar records nobody navigates to by name. - When a rename breaks dozens of pushes, that is the feature working: the compiler lists every caller to fix.
- Can you get typed routes without file-based routing?Yes. Since Vue Router 4.4 you can write a `RouteNamedMap` of `RouteRecordInfo` entries by hand and add it to `TypesConfig` with a `declare module 'vue-router'` augmentation. It gives the same checks, but you must keep the map in sync with the routes array yourself, which is why the docs recommend generating it.
- Why are raw params string | number while route.params are string?Raw params are what you pass in: the router stringifies a number such as a post id when it builds the URL. Normalized params are read back from a URL, which is text, so they are always strings; converting back to a number is your code's job.
saying these in an interview costs you the question
- Typed routes make router.push('/any/string') a compile error for unknown paths.
- useRoute('/posts/[slug]') throws at runtime when another route is active.
- Routes pushed onto the routes array at runtime appear in the generated types.
- The generated d.ts is build output and belongs in .gitignore.
- Typed params mean route.params.slug can be a number.