Skip to content

Commit 4726e2b

Browse files
committed
fix: allow undefined values for params in query
1 parent 4f58e5e commit 4726e2b

4 files changed

Lines changed: 120 additions & 31 deletions

File tree

packages/playground-file-based/src/routes.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ declare module 'vue-router/auto-routes' {
8686
'not-found': RouteRecordInfo<
8787
'not-found',
8888
'/:path(.*)',
89-
{ path: string, page?: number, other?: boolean, active?: boolean, multi?: string[], req?: number, optionalWhen?: Exclude<Param_date, unknown[] | null>, when?: Exclude<Param_date, unknown[] | null> },
89+
{ path: string, page?: number, other?: boolean | undefined, active?: boolean, multi?: string[] | undefined, req?: number, optionalWhen?: Exclude<Param_date, unknown[] | null> | undefined, when?: Exclude<Param_date, unknown[] | null> },
9090
{ path: string, page: number, other: boolean | undefined, active: boolean, multi: string[] | undefined, req: number, optionalWhen: Exclude<Param_date, unknown[] | null> | undefined, when: Exclude<Param_date, unknown[] | null> },
9191
| never
9292
>,
@@ -107,8 +107,8 @@ declare module 'vue-router/auto-routes' {
107107
'/b': RouteRecordInfo<
108108
'/b',
109109
'/b',
110-
{ test?: Param_set /* raw param parser */, date?: Extract<Param_date, unknown[]> },
111-
{ test: Param_set /* raw param parser */ | undefined, date: Extract<Param_date, unknown[]> | undefined },
110+
{ test?: Param_set /* raw param parser */ | undefined, date?: Extract<Param_date, unknown[]> | undefined },
111+
{ test: Param_set /* raw param parser */, date: Extract<Param_date, unknown[]> | undefined },
112112
| never
113113
>,
114114
'/blog/[slug]+': RouteRecordInfo<

packages/router/src/unplugin/codegen/generateRouteMap.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ describe('generateRouteNamedMap', () => {
953953
'/search': RouteRecordInfo<
954954
'/search',
955955
'/search',
956-
{ q?: string },
956+
{ q?: string | undefined },
957957
{ q: string | undefined },
958958
| never
959959
>,
@@ -1022,7 +1022,7 @@ describe('generateRouteNamedMap', () => {
10221022
'/search': RouteRecordInfo<
10231023
'/search',
10241024
'/search',
1025-
{ q?: string },
1025+
{ q?: string | undefined },
10261026
{ q: string | undefined },
10271027
| never
10281028
>,
@@ -1050,7 +1050,7 @@ describe('generateRouteNamedMap', () => {
10501050
'/search': RouteRecordInfo<
10511051
'/search',
10521052
'/search',
1053-
{ q: string, page?: number, sort?: string, filter?: number },
1053+
{ q: string, page?: number, sort?: string | undefined, filter?: number | undefined },
10541054
{ q: string, page: number, sort: string | undefined, filter: number | undefined },
10551055
| never
10561056
>,
@@ -1076,7 +1076,7 @@ describe('generateRouteNamedMap', () => {
10761076
'/search': RouteRecordInfo<
10771077
'/search',
10781078
'/search',
1079-
{ tags?: string[], ids: number[] },
1079+
{ tags?: string[] | undefined, ids: number[] },
10801080
{ tags: string[] | undefined, ids: number[] },
10811081
| never
10821082
>,

packages/router/src/unplugin/codegen/generateRouteParams.spec.ts

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ describe('EXPERIMENTAL_generateRouteParams', () => {
1313
return tree.insert(segment, `${segment}.vue`)
1414
}
1515

16+
function makeParsersMap(name: string, isRaw: boolean): ParamParsersMap {
17+
return new Map([
18+
[
19+
name,
20+
{
21+
name,
22+
typeName: `Param_${name}`,
23+
relativePath: `parsers/${name}`,
24+
absolutePath: `/abs/parsers/${name}`,
25+
isRaw,
26+
},
27+
],
28+
])
29+
}
30+
1631
describe('excludes null from custom parser types', () => {
1732
it('required path param excludes null', () => {
1833
const node = createTreeWithParam('[version=semver]')
@@ -74,21 +89,6 @@ describe('EXPERIMENTAL_generateRouteParams', () => {
7489
})
7590

7691
describe('raw param parsers', () => {
77-
function makeParsersMap(name: string, isRaw: boolean): ParamParsersMap {
78-
return new Map([
79-
[
80-
name,
81-
{
82-
name,
83-
typeName: `Param_${name}`,
84-
relativePath: `parsers/${name}`,
85-
absolutePath: `/abs/parsers/${name}`,
86-
isRaw,
87-
},
88-
],
89-
])
90-
}
91-
9292
it('emits Param_X /* raw param parser */ for raw path params', () => {
9393
const node = createTreeWithParam('[id=raw]')
9494
const result = EXPERIMENTAL_generateRouteParams(
@@ -154,4 +154,76 @@ describe('EXPERIMENTAL_generateRouteParams', () => {
154154
expect(result).toBe('{ id: Exclude<Param_plain, unknown[] | null> }')
155155
})
156156
})
157+
158+
describe('raw query param parsers', () => {
159+
function createNodeWithQueryParam(
160+
paramName: string,
161+
parserName: string
162+
): TreeNode {
163+
const tree = new PrefixTree(RESOLVED_OPTIONS)
164+
const node = tree.insert('b', 'b.vue')
165+
node.setCustomRouteBlock('b.vue', {
166+
params: {
167+
query: {
168+
[paramName]: { parser: parserName, format: 'value' },
169+
},
170+
},
171+
})
172+
return node
173+
}
174+
175+
it('omits | undefined on route.params for raw query parsers', () => {
176+
const node = createNodeWithQueryParam('test', 'set')
177+
// route.params side (isRaw=false): runtime always calls the raw parser
178+
// with the array form, so the value never ends up undefined.
179+
const result = EXPERIMENTAL_generateRouteParams(
180+
node,
181+
['Param_set'],
182+
false,
183+
makeParsersMap('set', true)
184+
)
185+
expect(result).toBe('{ test: Param_set /* raw param parser */ }')
186+
})
187+
188+
it('adds explicit | undefined on router.push for raw query parsers', () => {
189+
const node = createNodeWithQueryParam('test', 'set')
190+
// router.push side (isRaw=true): allow users to pass `undefined`
191+
// explicitly even under exactOptionalPropertyTypes.
192+
const result = EXPERIMENTAL_generateRouteParams(
193+
node,
194+
['Param_set'],
195+
true,
196+
makeParsersMap('set', true)
197+
)
198+
expect(result).toBe(
199+
'{ test?: Param_set /* raw param parser */ | undefined }'
200+
)
201+
})
202+
203+
it('keeps | undefined on route.params for non-raw query parsers', () => {
204+
const node = createNodeWithQueryParam('test', 'plain')
205+
const result = EXPERIMENTAL_generateRouteParams(
206+
node,
207+
['Param_plain'],
208+
false,
209+
makeParsersMap('plain', false)
210+
)
211+
expect(result).toBe(
212+
'{ test: Exclude<Param_plain, unknown[] | null> | undefined }'
213+
)
214+
})
215+
216+
it('adds | undefined on router.push for non-raw query parsers', () => {
217+
const node = createNodeWithQueryParam('test', 'plain')
218+
const result = EXPERIMENTAL_generateRouteParams(
219+
node,
220+
['Param_plain'],
221+
true,
222+
makeParsersMap('plain', false)
223+
)
224+
expect(result).toBe(
225+
'{ test?: Exclude<Param_plain, unknown[] | null> | undefined }'
226+
)
227+
})
228+
})
157229
})

packages/router/src/unplugin/codegen/generateRouteParams.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,23 @@ export function generateRouteParams(node: TreeNode, isRaw: boolean): string {
3939
'Record<never, never>'
4040
}
4141

42+
/**
43+
* Enhanced version of `generateRouteParams` that supports both path and query
44+
* params, and also takes into account the types of the params and whether they
45+
* are defined with raw parsers.
46+
*
47+
* @internal
48+
*
49+
* @param node - The tree node for which to generate the route params type.
50+
* @param types - An array of types corresponding to the params in the node. The order should match the order of params in the node.
51+
* @param isLoose - Whether to generate the type that is accepted when pushing (more persmissive)
52+
* @param paramParsersMap - An optional map of param parsers, used to determine if a param is defined with a raw parser.
53+
* @returns A string representing the TypeScript type for the route params of the given node.
54+
*/
4255
export function EXPERIMENTAL_generateRouteParams(
4356
node: TreeNode,
4457
types: Array<string | null>,
45-
isRaw: boolean,
58+
isLoose: boolean,
4659
paramParsersMap?: ParamParsersMap
4760
) {
4861
// node.params is a getter so we compute it once
@@ -54,6 +67,7 @@ export function EXPERIMENTAL_generateRouteParams(
5467
const isRepeatable = isTreeParamRepeatable(param)
5568
5669
const type = types[i]
70+
// if the param has a parser and is defined with defineParamParserRaw
5771
const isRawParser = !!(
5872
param.parser && paramParsersMap?.get(param.parser)?.isRaw
5973
)
@@ -82,13 +96,16 @@ export function EXPERIMENTAL_generateRouteParams(
8296
// Handle query params
8397
if (!param.required) {
8498
isOptionalQueryParam = true
85-
// For non-raw types (route.params), add explicit | undefined union
86-
// ONLY if there's no default value (with default, value is always present)
87-
if (
88-
!isRaw &&
89-
(param.defaultValue === undefined ||
90-
param.defaultValue === 'undefined')
91-
) {
99+
const hasNoDefault =
100+
param.defaultValue === undefined ||
101+
param.defaultValue === 'undefined'
102+
// For raw types (router.push), explicitly allow `undefined` so
103+
// the param is assignable even under `exactOptionalPropertyTypes`.
104+
// For non-raw types (route.params), only add `| undefined` when
105+
// the parser is not a raw parser: raw parsers always receive
106+
// the array form at runtime, so they never leave the value
107+
// undefined.
108+
if (hasNoDefault && (isLoose || !isRawParser)) {
92109
extractedType += ' | undefined'
93110
}
94111
}
@@ -97,7 +114,7 @@ export function EXPERIMENTAL_generateRouteParams(
97114
return `${param.paramName}${
98115
// For raw types (router.push), use ? marker for optional query params
99116
// For non-raw types (route.params), the | undefined is explicit in the union
100-
isRaw && isOptionalQueryParam ? '?' : ''
117+
isLoose && isOptionalQueryParam ? '?' : ''
101118
}: ${extractedType}`
102119
})
103120
.join(', ')} }`

0 commit comments

Comments
 (0)