Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 710a9de

Browse files
Anoesjposva
authored andcommitted
feat(types): add support for children routes as union (vuejs#2475)
Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
1 parent 0de3005 commit 710a9de

File tree

8 files changed

+171
-45
lines changed

8 files changed

+171
-45
lines changed

β€Žpackages/docs/guide/advanced/typed-routes.mdβ€Ž

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,43 @@ export interface RouteNamedMap {
2020
'home',
2121
// this is the path, it will appear in autocompletion
2222
'/',
23-
// these are the raw params. In this case, there are no params allowed
23+
// these are the raw params (what can be passed to router.push() and RouterLink's "to" prop)
24+
// In this case, there are no params allowed
2425
Record<never, never>,
25-
// these are the normalized params
26-
Record<never, never>
26+
// these are the normalized params (what you get from useRoute())
27+
Record<never, never>,
28+
// this is a union of all children route names, in this case, there are none
29+
never
2730
>
28-
// repeat for each route..
31+
// repeat for each route...
2932
// Note you can name them whatever you want
3033
'named-param': RouteRecordInfo<
3134
'named-param',
3235
'/:name',
33-
{ name: string | number }, // raw value
34-
{ name: string } // normalized value
36+
{ name: string | number }, // Allows string or number
37+
{ name: string }, // but always returns a string from the URL
38+
'named-param-edit'
39+
>
40+
'named-param-edit': RouteRecordInfo<
41+
'named-param-edit',
42+
'/:name/edit',
43+
{ name: string | number }, // we also include parent params
44+
{ name: string },
45+
never
3546
>
3647
'article-details': RouteRecordInfo<
3748
'article-details',
3849
'/articles/:id+',
3950
{ id: Array<number | string> },
40-
{ id: string[] }
51+
{ id: string[] },
52+
never
4153
>
4254
'not-found': RouteRecordInfo<
4355
'not-found',
4456
'/:path(.*)',
4557
{ path: string },
46-
{ path: string }
58+
{ path: string },
59+
never
4760
>
4861
}
4962

β€Žpackages/playground/src/main.tsβ€Ž

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,33 @@ app.use(router)
3232
window.vm = app.mount('#app')
3333

3434
export interface RouteNamedMap {
35-
home: RouteRecordInfo<'home', '/', Record<never, never>, Record<never, never>>
35+
home: RouteRecordInfo<
36+
'home',
37+
'/',
38+
Record<never, never>,
39+
Record<never, never>,
40+
never
41+
>
3642
'/[name]': RouteRecordInfo<
3743
'/[name]',
3844
'/:name',
3945
{ name: ParamValue<true> },
40-
{ name: ParamValue<false> }
46+
{ name: ParamValue<false> },
47+
'/[name]/edit'
48+
>
49+
'/[name]/edit': RouteRecordInfo<
50+
'/[name]/edit',
51+
'/:name/edit',
52+
{ name: ParamValue<true> },
53+
{ name: ParamValue<false> },
54+
never
4155
>
4256
'/[...path]': RouteRecordInfo<
4357
'/[...path]',
4458
'/:path(.*)',
4559
{ path: ParamValue<true> },
46-
{ path: ParamValue<false> }
60+
{ path: ParamValue<false> },
61+
never
4762
>
4863
}
4964

β€Žpackages/router/__tests__/routeLocation.test-d.tsβ€Ž

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,51 @@ import type {
77
RouteLocationNormalizedTypedList,
88
} from '../src'
99

10-
// TODO: could we move this to an .d.ts file that is only loaded for tests?
10+
// NOTE: A type allows us to make it work only in this test file
1111
// https://github.com/microsoft/TypeScript/issues/15300
1212
type RouteNamedMap = {
1313
home: RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
1414
'/[other]': RouteRecordInfo<
1515
'/[other]',
1616
'/:other',
1717
{ other: ParamValue<true> },
18-
{ other: ParamValue<false> }
18+
{ other: ParamValue<false> },
19+
never
1920
>
20-
'/[name]': RouteRecordInfo<
21-
'/[name]',
22-
'/:name',
23-
{ name: ParamValue<true> },
24-
{ name: ParamValue<false> }
21+
'/groups/[gid]': RouteRecordInfo<
22+
'/groups/[gid]',
23+
'/:gid',
24+
{ gid: ParamValue<true> },
25+
{ gid: ParamValue<false> },
26+
'/groups/[gid]/users' | '/groups/[gid]/users/[uid]'
27+
>
28+
'/groups/[gid]/users': RouteRecordInfo<
29+
'/groups/[gid]/users',
30+
'/:gid/users',
31+
{ gid: ParamValue<true> },
32+
{ gid: ParamValue<false> },
33+
'/groups/[gid]/users/[uid]'
34+
>
35+
'/groups/[gid]/users/[uid]': RouteRecordInfo<
36+
'/groups/[gid]/users/[uid]',
37+
'/:gid/users/:uid',
38+
{ gid: ParamValue<true>; uid: ParamValue<true> },
39+
{ gid: ParamValue<false>; uid: ParamValue<false> },
40+
never
2541
>
2642
'/[...path]': RouteRecordInfo<
2743
'/[...path]',
2844
'/:path(.*)',
2945
{ path: ParamValue<true> },
30-
{ path: ParamValue<false> }
46+
{ path: ParamValue<false> },
47+
never
3148
>
3249
'/deep/nesting/works/[[files]]+': RouteRecordInfo<
3350
'/deep/nesting/works/[[files]]+',
3451
'/deep/nesting/works/:files*',
3552
{ files?: ParamValueZeroOrMore<true> },
36-
{ files?: ParamValueZeroOrMore<false> }
53+
{ files?: ParamValueZeroOrMore<false> },
54+
never
3755
>
3856
}
3957

@@ -48,32 +66,50 @@ describe('Route Location types', () => {
4866
name: Name,
4967
fn: (to: RouteLocationNormalizedTypedList<RouteNamedMap>[Name]) => void
5068
): void
51-
function withRoute<Name extends RouteRecordName>(...args: unknown[]) {}
69+
function withRoute<_Name extends RouteRecordName>(..._args: unknown[]) {}
70+
71+
withRoute('/[other]', to => {
72+
expectTypeOf(to.params).toEqualTypeOf<{ other: string }>()
73+
expectTypeOf(to.params).not.toEqualTypeOf<{ gid: string }>()
74+
expectTypeOf(to.params).not.toEqualTypeOf<{ notExisting: string }>()
75+
})
76+
77+
withRoute('/groups/[gid]', to => {
78+
expectTypeOf(to.params).toEqualTypeOf<{ gid: string }>()
79+
expectTypeOf(to.params).not.toEqualTypeOf<{ notExisting: string }>()
80+
expectTypeOf(to.params).not.toEqualTypeOf<{ other: string }>()
81+
})
82+
83+
withRoute('/groups/[gid]/users', to => {
84+
expectTypeOf(to.params).toEqualTypeOf<{ gid: string }>()
85+
expectTypeOf(to.params).not.toEqualTypeOf<{ gid: string; uid: string }>()
86+
expectTypeOf(to.params).not.toEqualTypeOf<{ other: string }>()
87+
})
5288

53-
withRoute('/[name]', to => {
54-
expectTypeOf(to.params).toEqualTypeOf<{ name: string }>()
89+
withRoute('/groups/[gid]/users/[uid]', to => {
90+
expectTypeOf(to.params).toEqualTypeOf<{ gid: string;uid: string }>()
5591
expectTypeOf(to.params).not.toEqualTypeOf<{ notExisting: string }>()
5692
expectTypeOf(to.params).not.toEqualTypeOf<{ other: string }>()
5793
})
5894

59-
withRoute('/[name]' as keyof RouteNamedMap, to => {
95+
withRoute('/groups/[gid]' as keyof RouteNamedMap, to => {
6096
// @ts-expect-error: no all params have this
61-
to.params.name
62-
if (to.name === '/[name]') {
63-
to.params.name
97+
to.params.gid
98+
if (to.name === '/groups/[gid]') {
99+
to.params.gid
64100
// @ts-expect-error: no param other
65101
to.params.other
66102
}
67103
})
68104

69105
withRoute(to => {
70106
// @ts-expect-error: not all params object have a name
71-
to.params.name
107+
to.params.gid
72108
// @ts-expect-error: no route named like that
73109
if (to.name === '') {
74110
}
75-
if (to.name === '/[name]') {
76-
expectTypeOf(to.params).toEqualTypeOf<{ name: string }>()
111+
if (to.name === '/groups/[gid]') {
112+
expectTypeOf(to.params).toEqualTypeOf<{ gid: string }>()
77113
// @ts-expect-error: no param other
78114
to.params.other
79115
}

β€Žpackages/router/src/index.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export type {
113113
RouteLocationAsPathTypedList,
114114

115115
// route records
116+
RouteRecordInfoGeneric,
116117
RouteRecordInfo,
117118
RouteRecordNameGeneric,
118119
RouteRecordName,

β€Žpackages/router/src/typed-routes/route-map.tsβ€Ž

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import type { TypesConfig } from '../config'
2-
import type {
3-
RouteMeta,
4-
RouteParamsGeneric,
5-
RouteParamsRawGeneric,
6-
} from '../types'
2+
import type { RouteParamsGeneric, RouteParamsRawGeneric } from '../types'
73
import type { RouteRecord } from '../matcher/types'
84

95
/**
@@ -17,16 +13,30 @@ export interface RouteRecordInfo<
1713
// TODO: could probably be inferred from the Params
1814
ParamsRaw extends RouteParamsRawGeneric = RouteParamsRawGeneric,
1915
Params extends RouteParamsGeneric = RouteParamsGeneric,
20-
Meta extends RouteMeta = RouteMeta,
16+
// NOTE: this is the only type param that feels wrong because its default
17+
// value is the default value to avoid breaking changes but it should be the
18+
// generic version by default instead (string | symbol)
19+
ChildrenNames extends string | symbol = never,
20+
// TODO: implement meta with a defineRoute macro
21+
// Meta extends RouteMeta = RouteMeta,
2122
> {
2223
name: Name
2324
path: Path
2425
paramsRaw: ParamsRaw
2526
params: Params
27+
childrenNames: ChildrenNames
2628
// TODO: implement meta with a defineRoute macro
27-
meta: Meta
29+
// meta: Meta
2830
}
2931

32+
export type RouteRecordInfoGeneric = RouteRecordInfo<
33+
string | symbol,
34+
string,
35+
RouteParamsRawGeneric,
36+
RouteParamsGeneric,
37+
string | symbol
38+
>
39+
3040
/**
3141
* 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.
3242
*/
@@ -38,4 +48,4 @@ export type RouteMap =
3848
/**
3949
* Generic version of the `RouteMap`.
4050
*/
41-
export type RouteMapGeneric = Record<string | symbol, RouteRecordInfo>
51+
export type RouteMapGeneric = Record<string | symbol, RouteRecordInfoGeneric>

β€Žpackages/router/src/types/index.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export interface _RouteRecordBase extends PathParserOptions {
257257
* }
258258
* ```
259259
*/
260-
export interface RouteMeta extends Record<string|number|symbol, unknown> {}
260+
export interface RouteMeta extends Record<PropertyKey, unknown> {}
261261

262262
/**
263263
* Route Record defining one single component with the `component` option.

β€Žpackages/router/src/useApi.tsβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export function useRouter(): Router {
1818
*/
1919
export function useRoute<Name extends keyof RouteMap = keyof RouteMap>(
2020
_name?: Name
21-
): RouteLocationNormalizedLoaded<Name> {
22-
return inject(routeLocationKey)!
21+
) {
22+
return inject(routeLocationKey) as RouteLocationNormalizedLoaded<
23+
Name | RouteMap[Name]['childrenNames']
24+
>
2325
}

β€Žpackages/router/test-dts/typed-routes.test-d.tsβ€Ž

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
type RouteLocationTyped,
77
createRouter,
88
createWebHistory,
9+
useRoute,
10+
RouteLocationNormalizedLoadedTypedList,
911
} from './index'
1012

1113
// type is needed instead of an interface
@@ -15,23 +17,55 @@ export type RouteMap = {
1517
'/[...path]',
1618
'/:path(.*)',
1719
{ path: ParamValue<true> },
18-
{ path: ParamValue<false> }
20+
{ path: ParamValue<false> },
21+
never
1922
>
2023
'/[a]': RouteRecordInfo<
2124
'/[a]',
2225
'/:a',
2326
{ a: ParamValue<true> },
24-
{ a: ParamValue<false> }
27+
{ a: ParamValue<false> },
28+
never
29+
>
30+
'/a': RouteRecordInfo<
31+
'/a',
32+
'/a',
33+
Record<never, never>,
34+
Record<never, never>,
35+
'/a/b' | '/a/b/c'
36+
>
37+
'/a/b': RouteRecordInfo<
38+
'/a/b',
39+
'/a/b',
40+
Record<never, never>,
41+
Record<never, never>,
42+
'/a/b/c'
43+
>
44+
'/a/b/c': RouteRecordInfo<
45+
'/a/b/c',
46+
'/a/b/c',
47+
Record<never, never>,
48+
Record<never, never>,
49+
never
2550
>
26-
'/a': RouteRecordInfo<'/a', '/a', Record<never, never>, Record<never, never>>
2751
'/[id]+': RouteRecordInfo<
2852
'/[id]+',
2953
'/:id+',
3054
{ id: ParamValueOneOrMore<true> },
31-
{ id: ParamValueOneOrMore<false> }
55+
{ id: ParamValueOneOrMore<false> },
56+
never
3257
>
3358
}
3459

60+
// the type allows for type params to distribute types:
61+
// RouteLocationNormalizedLoadedLoaded<'/[a]' | '/'> will become RouteLocationNormalizedLoadedTyped<RouteMap>['/[a]'] | RouteLocationTypedList<RouteMap>['/']
62+
// it's closer to what the end users uses but with the RouteMap type fixed so it doesn't
63+
// pollute globals
64+
type RouteLocationNormalizedLoaded<
65+
Name extends keyof RouteMap = keyof RouteMap,
66+
> = RouteLocationNormalizedLoadedTypedList<RouteMap>[Name]
67+
// type Test = RouteLocationNormalizedLoaded<'/a' | '/a/b' | '/a/b/c'>
68+
3569
declare module './index' {
3670
interface TypesConfig {
3771
RouteNamedMap: RouteMap
@@ -136,4 +170,19 @@ describe('RouterTyped', () => {
136170
return true
137171
})
138172
})
173+
174+
it('useRoute', () => {
175+
expectTypeOf(useRoute('/[a]')).toEqualTypeOf<
176+
RouteLocationNormalizedLoaded<'/[a]'>
177+
>()
178+
expectTypeOf(useRoute('/a')).toEqualTypeOf<
179+
RouteLocationNormalizedLoaded<'/a' | '/a/b' | '/a/b/c'>
180+
>()
181+
expectTypeOf(useRoute('/a/b')).toEqualTypeOf<
182+
RouteLocationNormalizedLoaded<'/a/b' | '/a/b/c'>
183+
>()
184+
expectTypeOf(useRoute('/a/b/c')).toEqualTypeOf<
185+
RouteLocationNormalizedLoaded<'/a/b/c'>
186+
>()
187+
})
139188
})

0 commit comments

Comments
(0)

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /