'home',
// this is the path, it will appear in autocompletion
'/',
- // these are the raw params. In this case, there are no params allowed
+ // these are the raw params (what can be passed to router.push() and RouterLink's "to" prop)
+ // In this case, there are no params allowed
Record<never, never>,
- // these are the normalized params
- Record<never, never>
+ // these are the normalized params (what you get from useRoute())
+ Record<never, never>,
+ // this is a union of all children route names, in this case, there are none
+ never
>
- // repeat for each route..
+ // repeat for each route...
// Note you can name them whatever you want
'named-param': RouteRecordInfo<
'named-param',
'/:name',
- { name: string | number }, // raw value
- { name: string } // normalized value
+ { name: string | number }, // Allows string or number
+ { name: string }, // but always returns a string from the URL
+ 'named-param-edit'
+ >
+ 'named-param-edit': RouteRecordInfo<
+ 'named-param-edit',
+ '/:name/edit',
+ { name: string | number }, // we also include parent params
+ { name: string },
+ never
>
'article-details': RouteRecordInfo<
'article-details',
'/articles/:id+',
{ id: Array<number | string> },
- { id: string[] }
+ { id: string[] },
+ never
>
'not-found': RouteRecordInfo<
'not-found',
'/:path(.*)',
{ path: string },
- { path: string }
+ { path: string },
+ never
>
}
window.vm = app.mount('#app')
export interface RouteNamedMap {
- home: RouteRecordInfo<'home', '/', Record<never, never>, Record<never, never>>
+ home: RouteRecordInfo<
+ 'home',
+ '/',
+ Record<never, never>,
+ Record<never, never>,
+ never
+ >
'/[name]': RouteRecordInfo<
'/[name]',
'/:name',
{ name: ParamValue<true> },
- { name: ParamValue<false> }
+ { name: ParamValue<false> },
+ '/[name]/edit'
+ >
+ '/[name]/edit': RouteRecordInfo<
+ '/[name]/edit',
+ '/:name/edit',
+ { name: ParamValue<true> },
+ { name: ParamValue<false> },
+ never
>
'/[...path]': RouteRecordInfo<
'/[...path]',
'/:path(.*)',
{ path: ParamValue<true> },
- { path: ParamValue<false> }
+ { path: ParamValue<false> },
+ never
>
}
RouteLocationNormalizedTypedList,
} from '../src'
-// TODO: could we move this to an .d.ts file that is only loaded for tests?
+// NOTE: A type allows us to make it work only in this test file
// https://github.com/microsoft/TypeScript/issues/15300
type RouteNamedMap = {
home: RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
'/[other]',
'/:other',
{ other: ParamValue<true> },
- { other: ParamValue<false> }
+ { other: ParamValue<false> },
+ never
>
- '/[name]': RouteRecordInfo<
- '/[name]',
- '/:name',
- { name: ParamValue<true> },
- { name: ParamValue<false> }
+ '/groups/[gid]': RouteRecordInfo<
+ '/groups/[gid]',
+ '/:gid',
+ { gid: ParamValue<true> },
+ { gid: ParamValue<false> },
+ '/groups/[gid]/users' | '/groups/[gid]/users/[uid]'
+ >
+ '/groups/[gid]/users': RouteRecordInfo<
+ '/groups/[gid]/users',
+ '/:gid/users',
+ { gid: ParamValue<true> },
+ { gid: ParamValue<false> },
+ '/groups/[gid]/users/[uid]'
+ >
+ '/groups/[gid]/users/[uid]': RouteRecordInfo<
+ '/groups/[gid]/users/[uid]',
+ '/:gid/users/:uid',
+ { gid: ParamValue<true>; uid: ParamValue<true> },
+ { gid: ParamValue<false>; uid: ParamValue<false> },
+ never
>
'/[...path]': RouteRecordInfo<
'/[...path]',
'/:path(.*)',
{ path: ParamValue<true> },
- { path: ParamValue<false> }
+ { path: ParamValue<false> },
+ never
>
'/deep/nesting/works/[[files]]+': RouteRecordInfo<
'/deep/nesting/works/[[files]]+',
'/deep/nesting/works/:files*',
{ files?: ParamValueZeroOrMore<true> },
- { files?: ParamValueZeroOrMore<false> }
+ { files?: ParamValueZeroOrMore<false> },
+ never
>
}
name: Name,
fn: (to: RouteLocationNormalizedTypedList<RouteNamedMap>[Name]) => void
): void
- function withRoute<Name extends RouteRecordName>(...args: unknown[]) {}
+ function withRoute<_Name extends RouteRecordName>(..._args: unknown[]) {}
+
+ withRoute('/[other]', to => {
+ expectTypeOf(to.params).toEqualTypeOf<{ other: string }>()
+ expectTypeOf(to.params).not.toEqualTypeOf<{ gid: string }>()
+ expectTypeOf(to.params).not.toEqualTypeOf<{ notExisting: string }>()
+ })
+
+ withRoute('/groups/[gid]', to => {
+ expectTypeOf(to.params).toEqualTypeOf<{ gid: string }>()
+ expectTypeOf(to.params).not.toEqualTypeOf<{ notExisting: string }>()
+ expectTypeOf(to.params).not.toEqualTypeOf<{ other: string }>()
+ })
+
+ withRoute('/groups/[gid]/users', to => {
+ expectTypeOf(to.params).toEqualTypeOf<{ gid: string }>()
+ expectTypeOf(to.params).not.toEqualTypeOf<{ gid: string; uid: string }>()
+ expectTypeOf(to.params).not.toEqualTypeOf<{ other: string }>()
+ })
- withRoute('/[name]', to => {
- expectTypeOf(to.params).toEqualTypeOf<{ name: string }>()
+ withRoute('/groups/[gid]/users/[uid]', to => {
+ expectTypeOf(to.params).toEqualTypeOf<{ gid: string; uid: string }>()
expectTypeOf(to.params).not.toEqualTypeOf<{ notExisting: string }>()
expectTypeOf(to.params).not.toEqualTypeOf<{ other: string }>()
})
- withRoute('/[name]' as keyof RouteNamedMap, to => {
+ withRoute('/groups/[gid]' as keyof RouteNamedMap, to => {
// @ts-expect-error: no all params have this
- to.params.name
- if (to.name === '/[name]') {
- to.params.name
+ to.params.gid
+ if (to.name === '/groups/[gid]') {
+ to.params.gid
// @ts-expect-error: no param other
to.params.other
}
withRoute(to => {
// @ts-expect-error: not all params object have a name
- to.params.name
+ to.params.gid
// @ts-expect-error: no route named like that
if (to.name === '') {
}
- if (to.name === '/[name]') {
- expectTypeOf(to.params).toEqualTypeOf<{ name: string }>()
+ if (to.name === '/groups/[gid]') {
+ expectTypeOf(to.params).toEqualTypeOf<{ gid: string }>()
// @ts-expect-error: no param other
to.params.other
}
RouteLocationAsPathTypedList,
// route records
+ RouteRecordInfoGeneric,
RouteRecordInfo,
RouteRecordNameGeneric,
RouteRecordName,
import type { TypesConfig } from '../config'
-import type {
- RouteMeta,
- RouteParamsGeneric,
- RouteParamsRawGeneric,
-} from '../types'
+import type { RouteParamsGeneric, RouteParamsRawGeneric } from '../types'
import type { RouteRecord } from '../matcher/types'
/**
// TODO: could probably be inferred from the Params
ParamsRaw extends RouteParamsRawGeneric = RouteParamsRawGeneric,
Params extends RouteParamsGeneric = RouteParamsGeneric,
- Meta extends RouteMeta = RouteMeta,
+ // NOTE: this is the only type param that feels wrong because its default
+ // value is the default value to avoid breaking changes but it should be the
+ // generic version by default instead (string | symbol)
+ ChildrenNames extends string | symbol = never,
+ // TODO: implement meta with a defineRoute macro
+ // Meta extends RouteMeta = RouteMeta,
> {
name: Name
path: Path
paramsRaw: ParamsRaw
params: Params
+ childrenNames: ChildrenNames
// TODO: implement meta with a defineRoute macro
- meta: Meta
+ // meta: Meta
}
+export type RouteRecordInfoGeneric = RouteRecordInfo<
+ string | symbol,
+ string,
+ RouteParamsRawGeneric,
+ RouteParamsGeneric,
+ string | symbol
+>
+
/**
* Convenience type to get the typed RouteMap or a generic one if not provided. It is extracted from the {@link TypesConfig} if it exists, it becomes {@link RouteMapGeneric} otherwise.
*/
/**
* Generic version of the `RouteMap`.
*/
-export type RouteMapGeneric = Record<string | symbol, RouteRecordInfo>
+export type RouteMapGeneric = Record<string | symbol, RouteRecordInfoGeneric>
* }
* ```
*/
-export interface RouteMeta extends Record<string | number | symbol, unknown> {}
+export interface RouteMeta extends Record<PropertyKey, unknown> {}
/**
* Route Record defining one single component with the `component` option.
*/
export function useRoute<Name extends keyof RouteMap = keyof RouteMap>(
_name?: Name
-): RouteLocationNormalizedLoaded<Name> {
- return inject(routeLocationKey)!
+) {
+ return inject(routeLocationKey) as RouteLocationNormalizedLoaded<
+ Name | RouteMap[Name]['childrenNames']
+ >
}
type RouteLocationTyped,
createRouter,
createWebHistory,
+ useRoute,
+ RouteLocationNormalizedLoadedTypedList,
} from './index'
// type is needed instead of an interface
'/[...path]',
'/:path(.*)',
{ path: ParamValue<true> },
- { path: ParamValue<false> }
+ { path: ParamValue<false> },
+ never
>
'/[a]': RouteRecordInfo<
'/[a]',
'/:a',
{ a: ParamValue<true> },
- { a: ParamValue<false> }
+ { a: ParamValue<false> },
+ never
+ >
+ '/a': RouteRecordInfo<
+ '/a',
+ '/a',
+ Record<never, never>,
+ Record<never, never>,
+ '/a/b' | '/a/b/c'
+ >
+ '/a/b': RouteRecordInfo<
+ '/a/b',
+ '/a/b',
+ Record<never, never>,
+ Record<never, never>,
+ '/a/b/c'
+ >
+ '/a/b/c': RouteRecordInfo<
+ '/a/b/c',
+ '/a/b/c',
+ Record<never, never>,
+ Record<never, never>,
+ never
>
- '/a': RouteRecordInfo<'/a', '/a', Record<never, never>, Record<never, never>>
'/[id]+': RouteRecordInfo<
'/[id]+',
'/:id+',
{ id: ParamValueOneOrMore<true> },
- { id: ParamValueOneOrMore<false> }
+ { id: ParamValueOneOrMore<false> },
+ never
>
}
+// the type allows for type params to distribute types:
+// RouteLocationNormalizedLoadedLoaded<'/[a]' | '/'> will become RouteLocationNormalizedLoadedTyped<RouteMap>['/[a]'] | RouteLocationTypedList<RouteMap>['/']
+// it's closer to what the end users uses but with the RouteMap type fixed so it doesn't
+// pollute globals
+type RouteLocationNormalizedLoaded<
+ Name extends keyof RouteMap = keyof RouteMap,
+> = RouteLocationNormalizedLoadedTypedList<RouteMap>[Name]
+// type Test = RouteLocationNormalizedLoaded<'/a' | '/a/b' | '/a/b/c'>
+
declare module './index' {
interface TypesConfig {
RouteNamedMap: RouteMap
return true
})
})
+
+ it('useRoute', () => {
+ expectTypeOf(useRoute('/[a]')).toEqualTypeOf<
+ RouteLocationNormalizedLoaded<'/[a]'>
+ >()
+ expectTypeOf(useRoute('/a')).toEqualTypeOf<
+ RouteLocationNormalizedLoaded<'/a' | '/a/b' | '/a/b/c'>
+ >()
+ expectTypeOf(useRoute('/a/b')).toEqualTypeOf<
+ RouteLocationNormalizedLoaded<'/a/b' | '/a/b/c'>
+ >()
+ expectTypeOf(useRoute('/a/b/c')).toEqualTypeOf<
+ RouteLocationNormalizedLoaded<'/a/b/c'>
+ >()
+ })
})