Skip to content

Commit eb1f989

Browse files
fix: do not use intersection for search and path params (#867)
* fix: do not use intersection for search params * fix: treat path params the same as search params w.r.t. intersection / union handling
1 parent d0bc0e5 commit eb1f989

File tree

4 files changed

+95
-115
lines changed

4 files changed

+95
-115
lines changed

packages/react-router/src/RouterProvider.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export interface MatchLocation {
4040
}
4141

4242
export type NavigateFn<TRouteTree extends AnyRoute> = <
43-
TFrom extends RoutePaths<TRouteTree> = '/',
44-
TTo extends string = '',
45-
TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
46-
TMaskTo extends string = '',
43+
TFrom extends RoutePaths<TRouteTree> | string = string,
44+
TTo extends string | undefined = undefined,
45+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
46+
TMaskTo extends string | undefined = undefined,
4747
>(
4848
opts: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
4949
) => Promise<void>

packages/react-router/src/link.tsx

Lines changed: 67 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
PickRequired,
2020
UnionToIntersection,
2121
Updater,
22+
WithoutEmpty,
2223
deepEqual,
2324
functionalUpdate,
2425
} from './utils'
@@ -116,10 +117,10 @@ export type RelativeToPathAutoComplete<
116117

117118
export type NavigateOptions<
118119
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
119-
TFrom extends RoutePaths<TRouteTree> = '/',
120-
TTo extends string = '',
121-
TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
122-
TMaskTo extends string = '',
120+
TFrom extends RoutePaths<TRouteTree> | string = string,
121+
TTo extends string | undefined = undefined,
122+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
123+
TMaskTo extends string | undefined = undefined,
123124
> = ToOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
124125
// `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one.
125126
replace?: boolean
@@ -130,26 +131,26 @@ export type NavigateOptions<
130131

131132
export type ToOptions<
132133
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
133-
TFrom extends RoutePaths<TRouteTree> = '/',
134-
TTo extends string = '',
135-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
136-
TMaskTo extends string = '',
134+
TFrom extends RoutePaths<TRouteTree> | string = string,
135+
TTo extends string | undefined = undefined,
136+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
137+
TMaskTo extends string | undefined = undefined,
137138
> = ToSubOptions<TRouteTree, TFrom, TTo> & {
138139
mask?: ToMaskOptions<TRouteTree, TMaskFrom, TMaskTo>
139140
}
140141

141142
export type ToMaskOptions<
142143
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
143-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
144-
TMaskTo extends string = '',
144+
TMaskFrom extends RoutePaths<TRouteTree> | string = string,
145+
TMaskTo extends string | undefined = undefined,
145146
> = ToSubOptions<TRouteTree, TMaskFrom, TMaskTo> & {
146147
unmaskOnReload?: boolean
147148
}
148149

149150
export type ToSubOptions<
150151
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
151-
TFrom extends RoutePaths<TRouteTree> = '/',
152-
TTo extends string = '',
152+
TFrom extends RoutePaths<TRouteTree> | string = string,
153+
TTo extends string | undefined = undefined,
153154
TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
154155
> = {
155156
to?: ToPathOption<TRouteTree, TFrom, TTo>
@@ -162,72 +163,56 @@ export type ToSubOptions<
162163
// // When using relative route paths, this option forces resolution from the current path, instead of the route API's path or `from` path
163164
} & CheckPath<TRouteTree, NoInfer<TResolved>, {}> &
164165
SearchParamOptions<TRouteTree, TFrom, TTo, TResolved> &
165-
PathParamOptions<TRouteTree, TFrom, TResolved>
166+
PathParamOptions<TRouteTree, TFrom, TTo, TResolved>
166167

167-
export type SearchParamOptions<
168+
type ParamsReducer<TFrom, TTo> = TTo | ((current: TFrom) => TTo)
169+
170+
export type ParamOptions<
168171
TRouteTree extends AnyRoute,
169172
TFrom,
170173
TTo,
171-
TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
172-
TFromSearchEnsured = '/' extends TFrom
173-
? FullSearchSchema<TRouteTree>
174-
: Expand<
175-
PickRequired<
176-
RouteByPath<TRouteTree, TFrom>['types']['fullSearchSchema']
177-
>
178-
>,
179-
TFromSearchOptional = Omit<
180-
FullSearchSchema<TRouteTree>,
181-
keyof TFromSearchEnsured
182-
>,
183-
TFromSearch = Expand<TFromSearchEnsured & TFromSearchOptional>,
184-
TToSearch = '' extends TTo
185-
? FullSearchSchema<TRouteTree>
186-
: Expand<RouteByPath<TRouteTree, TResolved>['types']['fullSearchSchema']>,
187-
> = keyof PickRequired<TToSearch> extends never
188-
? {
189-
search?: true | SearchReducer<TFromSearch, TToSearch>
190-
}
191-
: {
192-
search: TFromSearchEnsured extends PickRequired<TToSearch>
193-
? true | SearchReducer<TFromSearch, TToSearch>
194-
: SearchReducer<TFromSearch, TToSearch>
195-
}
174+
TResolved,
175+
TParamVariant extends 'allParams' | 'fullSearchSchema',
176+
TFromParams = Expand<RouteByPath<TRouteTree, TFrom>['types'][TParamVariant]>,
177+
TToParams = TTo extends undefined
178+
? TFromParams
179+
: never extends TResolved
180+
? Expand<RouteByPath<TRouteTree, TTo>['types'][TParamVariant]>
181+
: Expand<RouteByPath<TRouteTree, TResolved>['types'][TParamVariant]>,
182+
TReducer = ParamsReducer<TFromParams, TToParams>,
183+
> = Expand<WithoutEmpty<PickRequired<TToParams>>> extends never
184+
? Partial<MakeParamOption<TParamVariant, true | TReducer>>
185+
: TFromParams extends Expand<WithoutEmpty<PickRequired<TToParams>>>
186+
? MakeParamOption<TParamVariant, true | TReducer>
187+
: MakeParamOption<TParamVariant, TReducer>
188+
189+
type MakeParamOption<
190+
TParamVariant extends 'allParams' | 'fullSearchSchema',
191+
T,
192+
> = TParamVariant extends 'allParams'
193+
? MakePathParamOptions<T>
194+
: MakeSearchParamOptions<T>
195+
type MakeSearchParamOptions<T> = { search: T }
196+
type MakePathParamOptions<T> = { params: T }
196197

197-
type SearchReducer<TFrom, TTo> = TTo | ((current: TFrom) => TTo)
198+
export type SearchParamOptions<
199+
TRouteTree extends AnyRoute,
200+
TFrom,
201+
TTo,
202+
TResolved,
203+
> = ParamOptions<TRouteTree, TFrom, TTo, TResolved, 'fullSearchSchema'>
198204

199205
export type PathParamOptions<
200206
TRouteTree extends AnyRoute,
201207
TFrom,
202208
TTo,
203-
TFromParamsEnsured = Expand<
204-
UnionToIntersection<
205-
PickRequired<RouteByPath<TRouteTree, TFrom>['types']['allParams']>
206-
>
207-
>,
208-
TFromParamsOptional = Omit<AllParams<TRouteTree>, keyof TFromParamsEnsured>,
209-
TFromParams = Expand<TFromParamsOptional & TFromParamsEnsured>,
210-
TToParams = Expand<RouteByPath<TRouteTree, TTo>['types']['allParams']>,
211-
> = never extends TToParams
212-
? {
213-
params?: true | ParamsReducer<Partial<TFromParams>, Partial<TFromParams>>
214-
}
215-
: keyof PickRequired<TToParams> extends never
216-
? {
217-
params?: true | ParamsReducer<TFromParams, TToParams>
218-
}
219-
: {
220-
params: TFromParamsEnsured extends PickRequired<TToParams>
221-
? true | ParamsReducer<TFromParams, TToParams>
222-
: ParamsReducer<TFromParams, TToParams>
223-
}
224-
225-
type ParamsReducer<TFrom, TTo> = TTo | ((current: TFrom) => TTo)
209+
TResolved,
210+
> = ParamOptions<TRouteTree, TFrom, TTo, TResolved, 'allParams'>
226211

227212
export type ToPathOption<
228213
TRouteTree extends AnyRoute = AnyRoute,
229-
TFrom extends RoutePaths<TRouteTree> = '/',
230-
TTo extends string = '',
214+
TFrom extends RoutePaths<TRouteTree> | string = string,
215+
TTo extends string | undefined = undefined,
231216
> =
232217
| TTo
233218
| RelativeToPathAutoComplete<
@@ -238,8 +223,8 @@ export type ToPathOption<
238223

239224
export type ToIdOption<
240225
TRouteTree extends AnyRoute = AnyRoute,
241-
TFrom extends RoutePaths<TRouteTree> = '/',
242-
TTo extends string = '',
226+
TFrom extends RoutePaths<TRouteTree> | undefined = undefined,
227+
TTo extends string | undefined = undefined,
243228
> =
244229
| TTo
245230
| RelativeToPathAutoComplete<
@@ -256,10 +241,10 @@ export interface ActiveOptions {
256241

257242
export type LinkOptions<
258243
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
259-
TFrom extends RoutePaths<TRouteTree> = '/',
260-
TTo extends string = '',
261-
TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
262-
TMaskTo extends string = '',
244+
TFrom extends RoutePaths<TRouteTree> | string = string,
245+
TTo extends string | undefined = undefined,
246+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
247+
TMaskTo extends string | undefined = undefined,
263248
> = NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
264249
// The standard anchor tag target attribute
265250
target?: HTMLAnchorElement['target']
@@ -348,20 +333,13 @@ const preloadWarning = 'Error preloading route! ☝️'
348333

349334
export function useLinkProps<
350335
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
351-
TFrom extends RoutePaths<TRouteTree> = '/',
352-
TTo extends string = '',
353-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
354-
TMaskTo extends string = '',
355-
>({
356-
from,
357-
...options
358-
}: UseLinkPropsOptions<
359-
TRouteTree,
360-
TFrom,
361-
TTo,
362-
TMaskFrom,
363-
TMaskTo
364-
>): React.AnchorHTMLAttributes<HTMLAnchorElement> {
336+
TFrom extends RoutePaths<TRouteTree> | string = string,
337+
TTo extends string | undefined = undefined,
338+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
339+
TMaskTo extends string | undefined = undefined,
340+
>(
341+
options: UseLinkPropsOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
342+
): React.AnchorHTMLAttributes<HTMLAnchorElement> {
365343
const router = useRouter()
366344
const matchPathname = useMatch({
367345
strict: false,
@@ -574,10 +552,10 @@ export function useLinkProps<
574552
export interface LinkComponent<TProps extends Record<string, any> = {}> {
575553
<
576554
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
577-
TFrom extends RoutePaths<TRouteTree> = '/',
578-
TTo extends string = '',
579-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
580-
TMaskTo extends string = '',
555+
TFrom extends RoutePaths<TRouteTree> | string = string,
556+
TTo extends string | undefined = undefined,
557+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
558+
TMaskTo extends string | undefined = undefined,
581559
>(
582560
props: LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
583561
TProps &

packages/react-router/src/useNavigate.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import { LinkOptions, NavigateOptions } from './link'
55
import { AnyRoute } from './route'
66
import { RoutePaths } from './routeInfo'
77
import { RegisteredRouter } from './router'
8-
import { useLayoutEffect } from './utils'
98

109
export function useNavigate<
1110
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
12-
TDefaultFrom extends RoutePaths<TRouteTree> = '/',
11+
TDefaultFrom extends RoutePaths<TRouteTree> | string = string,
1312
>(_defaultOpts?: { from?: TDefaultFrom }) {
1413
const { navigate, buildLocation } = useRouter()
1514

@@ -20,10 +19,10 @@ export function useNavigate<
2019

2120
return React.useCallback(
2221
<
23-
TFrom extends RoutePaths<TRouteTree> = TDefaultFrom,
24-
TTo extends string = '',
25-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
26-
TMaskTo extends string = '',
22+
TFrom extends RoutePaths<TRouteTree> | string = TDefaultFrom,
23+
TTo extends string | undefined = undefined,
24+
TMaskFrom extends RoutePaths<TRouteTree>| string = TFrom,
25+
TMaskTo extends string | undefined = undefined,
2726
>({
2827
from,
2928
...rest
@@ -54,10 +53,10 @@ export function useNavigate<
5453

5554
export function Navigate<
5655
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
57-
TFrom extends RoutePaths<TRouteTree> = '/',
58-
TTo extends string = '',
59-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
60-
TMaskTo extends string = '',
56+
TFrom extends RoutePaths<TRouteTree> | string = string,
57+
TTo extends string | undefined = undefined,
58+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
59+
TMaskTo extends string | undefined = undefined,
6160
>(props: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>): null {
6261
const { navigate } = useRouter()
6362
const match = useMatch({ strict: false })
@@ -74,19 +73,19 @@ export function Navigate<
7473

7574
export type UseLinkPropsOptions<
7675
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
77-
TFrom extends RoutePaths<TRouteTree> = '/',
78-
TTo extends string = '',
79-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
80-
TMaskTo extends string = '',
76+
TFrom extends RoutePaths<TRouteTree> | string = string,
77+
TTo extends string | undefined= undefined,
78+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
79+
TMaskTo extends string | undefined = undefined,
8180
> = ActiveLinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
8281
React.AnchorHTMLAttributes<HTMLAnchorElement>
8382

8483
export type LinkProps<
8584
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
86-
TFrom extends RoutePaths<TRouteTree> = '/',
87-
TTo extends string = '',
88-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
89-
TMaskTo extends string = '',
85+
TFrom extends RoutePaths<TRouteTree> | string = string,
86+
TTo extends string| undefined = undefined,
87+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
88+
TMaskTo extends string | undefined = undefined,
9089
> = ActiveLinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
9190
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
9291
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
@@ -97,10 +96,10 @@ export type LinkProps<
9796

9897
export type ActiveLinkOptions<
9998
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
100-
TFrom extends RoutePaths<TRouteTree> = '/',
101-
TTo extends string = '',
102-
TMaskFrom extends RoutePaths<TRouteTree> = '/',
103-
TMaskTo extends string = '',
99+
TFrom extends RoutePaths<TRouteTree> | string = string,
100+
TTo extends string | undefined = undefined,
101+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
102+
TMaskTo extends string | undefined = undefined,
104103
> = LinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
105104
// A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
106105
activeProps?:

packages/react-router/src/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export type PickRequired<T> = {
2626
[K in keyof T as undefined extends T[K] ? never : K]: T[K]
2727
}
2828

29+
// from https://stackoverflow.com/a/76458160
30+
export type WithoutEmpty<T> = T extends T ? ({} extends T ? never : T) : never
31+
2932
// export type Expand<T> = T
3033
export type Expand<T> = T extends object
3134
? T extends infer O

0 commit comments

Comments
 (0)