From 6f51802a6381aee3a1e6955ae576255025e2521c Mon Sep 17 00:00:00 2001 From: chorobin Date: Thu, 2 Jan 2025 23:11:24 +0100 Subject: [PATCH 1/2] fix: make `to` required if `from` is not set --- packages/react-router/src/link.tsx | 33 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index a26b8d5543..73cd050a42 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -234,16 +234,27 @@ export type ToSubOptions< SearchParamOptions & PathParamOptions -export interface ToSubOptionsProps< - in out TRouter extends AnyRouter = RegisteredRouter, - in out TFrom extends RoutePaths | string = string, - in out TTo extends string | undefined = '.', -> { - to?: ToPathOption & {} +export type MakeToRequired< + TRouter extends AnyRouter, + TFrom extends string, + TTo extends string | undefined, +> = string extends TFrom + ? { + to: ToPathOption & {} + from?: FromPathOption & {} + } + : { + to?: ToPathOption & {} + from?: FromPathOption & {} + } + +export type ToSubOptionsProps< + TRouter extends AnyRouter = RegisteredRouter, + TFrom extends RoutePaths | string = string, + TTo extends string | undefined = '.', +> = MakeToRequired & { hash?: true | Updater state?: true | NonNullableUpdater - // The source route path. This is automatically set when using route-level APIs, but for type-safe relative routing on the router itself, this is required - from?: FromPathOption & {} } export type ParamsReducerFn< @@ -1014,7 +1025,11 @@ export function createLink( export const Link: LinkComponent<'a'> = React.forwardRef( (props, ref) => { const { _asChild, ...rest } = props - const { type: _type, ref: innerRef, ...linkProps } = useLinkProps(rest, ref) + const { + type: _type, + ref: innerRef, + ...linkProps + } = useLinkProps(rest as any, ref) const children = typeof rest.children === 'function' From 293096ec5fe7c4a763adea0b5c930ad3dd61388b Mon Sep 17 00:00:00 2001 From: chorobin Date: Sat, 4 Jan 2025 12:43:35 +0100 Subject: [PATCH 2/2] chore: fix build --- .../basic-file-based/src/routes/anchor.tsx | 1 + .../basic-file-based/src/routes/anchor.tsx | 1 + packages/react-router/src/Matches.tsx | 33 +++---- packages/react-router/src/index.tsx | 10 +- packages/react-router/src/link.tsx | 97 ++++++++++--------- packages/react-router/src/routeInfo.ts | 43 ++++---- .../react-router/tests/Matches.test-d.tsx | 18 +--- packages/react-router/tests/link.test-d.tsx | 22 +---- .../react-router/tests/redirects.test-d.tsx | 4 +- packages/react-router/tests/router.test-d.tsx | 4 +- .../react-router/tests/useNavigate.test-d.tsx | 4 +- .../tests/generator/nested/tests.test-d.ts | 5 - 12 files changed, 104 insertions(+), 138 deletions(-) diff --git a/e2e/react-router/basic-file-based/src/routes/anchor.tsx b/e2e/react-router/basic-file-based/src/routes/anchor.tsx index c0c8cdd8a0..93975ba621 100644 --- a/e2e/react-router/basic-file-based/src/routes/anchor.tsx +++ b/e2e/react-router/basic-file-based/src/routes/anchor.tsx @@ -83,6 +83,7 @@ function AnchorComponent() { {anchors.map((anchor) => (
  • (
  • | string = RoutePaths< - TRouter['routeTree'] - >, - TTo extends string | undefined = '', - TMaskFrom extends RoutePaths | string = TFrom, + TFrom extends string = string, + TTo extends string | undefined = undefined, + TMaskFrom extends string = TFrom, TMaskTo extends string = '', - TOptions extends ToOptions< - TRouter, - TFrom, - TTo, - TMaskFrom, - TMaskTo - > = ToOptions, - TRelaxedOptions = Omit & - DeepPartial>, -> = TRelaxedOptions & MatchRouteOptions +> = ToSubOptionsProps & + DeepPartial> & + DeepPartial> & + MaskOptions & + MatchRouteOptions export function useMatchRoute() { const router = useRouter() diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx index 3a4ea4f4e4..8ac5a9d53b 100644 --- a/packages/react-router/src/index.tsx +++ b/packages/react-router/src/index.tsx @@ -47,11 +47,11 @@ export type { ParsePathParams, RemoveTrailingSlashes, RemoveLeadingSlashes, - SearchPaths, - SearchRelativePathAutoComplete, - RelativeToParentPathAutoComplete, - RelativeToCurrentPathAutoComplete, - AbsolutePathAutoComplete, + InferDescendantToPaths, + RelativeToPath, + RelativeToParentPath, + RelativeToCurrentPath, + AbsoluteToPath, RelativeToPathAutoComplete, NavigateOptions, ToOptions, diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index 73cd050a42..2b2a2fb034 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -27,7 +27,6 @@ import type { RoutePaths, RouteToPath, ToPath, - TrailingSlashOptionByRouter, } from './routeInfo' import type { AnyRouter, @@ -80,78 +79,73 @@ export type RemoveLeadingSlashes = T extends `/${string}` : T : T -export type FindDescendantPaths< +export type FindDescendantToPaths< TRouter extends AnyRouter, TPrefix extends string, > = `${TPrefix}/${string}` & RouteToPath -export type SearchPaths< +export type InferDescendantToPaths< TRouter extends AnyRouter, TPrefix extends string, - TPaths = FindDescendantPaths, + TPaths = FindDescendantToPaths, > = TPaths extends `${TPrefix}/` ? never : TPaths extends `${TPrefix}/${infer TRest}` ? TRest : never -export type SearchRelativePathAutoComplete< +export type RelativeToPath< TRouter extends AnyRouter, TTo extends string, - TSearchPath extends string, + TResolvedPath extends string, > = - | (TSearchPath & RouteToPath extends never + | (TResolvedPath & RouteToPath extends never ? never - : ToPath, TTo>) - | `${TTo}/${SearchPaths>}` + : ToPath) + | `${RemoveTrailingSlashes}/${InferDescendantToPaths>}` -export type RelativeToParentPathAutoComplete< +export type RelativeToParentPath< TRouter extends AnyRouter, TFrom extends string, TTo extends string, TResolvedPath extends string = ResolveRelativePath, > = - | SearchRelativePathAutoComplete + | RelativeToPath | (TTo extends `${string}..` | `${string}../` ? TResolvedPath extends '/' | '' ? never - : FindDescendantPaths< + : FindDescendantToPaths< TRouter, RemoveTrailingSlashes > extends never ? never - : `${TTo}/${ParentPath>}` + : `${RemoveTrailingSlashes}/${ParentPath}` : never) -export type RelativeToCurrentPathAutoComplete< +export type RelativeToCurrentPath< TRouter extends AnyRouter, TFrom extends string, TTo extends string, TResolvedPath extends string = ResolveRelativePath, -> = - | SearchRelativePathAutoComplete - | CurrentPath> +> = RelativeToPath | CurrentPath -export type AbsolutePathAutoComplete< - TRouter extends AnyRouter, - TFrom extends string, -> = +export type AbsoluteToPath = | (string extends TFrom - ? CurrentPath> + ? CurrentPath : TFrom extends `/` ? never - : CurrentPath>) + : CurrentPath) | (string extends TFrom - ? ParentPath> + ? ParentPath : TFrom extends `/` ? never - : ParentPath>) + : ParentPath) | RouteToPath | (TFrom extends '/' ? never : string extends TFrom ? never - : SearchPaths>) + : InferDescendantToPaths>) export type RelativeToPathAutoComplete< TRouter extends AnyRouter, @@ -160,20 +154,12 @@ export type RelativeToPathAutoComplete< > = string extends TTo ? string : string extends TFrom - ? AbsolutePathAutoComplete + ? AbsoluteToPath : TTo & `..${string}` extends never ? TTo & `.${string}` extends never - ? AbsolutePathAutoComplete - : RelativeToCurrentPathAutoComplete< - TRouter, - TFrom, - RemoveTrailingSlashes - > - : RelativeToParentPathAutoComplete< - TRouter, - TFrom, - RemoveTrailingSlashes - > + ? AbsoluteToPath + : RelativeToCurrentPath + : RelativeToParentPath export type NavigateOptions< TRouter extends AnyRouter = RegisteredRouter, @@ -234,19 +220,33 @@ export type ToSubOptions< SearchParamOptions & PathParamOptions +export interface RequiredToOptions< + in out TRouter extends AnyRouter, + in out TFrom extends string, + in out TTo extends string | undefined, +> { + to: ToPathOption & {} +} + +export interface OptionalToOptions< + in out TRouter extends AnyRouter, + in out TFrom extends string, + in out TTo extends string | undefined, +> { + to?: ToPathOption & {} +} + export type MakeToRequired< TRouter extends AnyRouter, TFrom extends string, TTo extends string | undefined, > = string extends TFrom - ? { - to: ToPathOption & {} - from?: FromPathOption & {} - } - : { - to?: ToPathOption & {} - from?: FromPathOption & {} - } + ? string extends TTo + ? OptionalToOptions + : TTo & CatchAllPaths extends never + ? RequiredToOptions + : OptionalToOptions + : OptionalToOptions export type ToSubOptionsProps< TRouter extends AnyRouter = RegisteredRouter, @@ -255,6 +255,7 @@ export type ToSubOptionsProps< > = MakeToRequired & { hash?: true | Updater state?: true | NonNullableUpdater + from?: FromPathOption & {} } export type ParamsReducerFn< @@ -330,7 +331,7 @@ export type ResolveToParams< ? never : string extends TPath ? ResolveAllToParams - : TPath extends CatchAllPaths> + : TPath extends CatchAllPaths ? ResolveAllToParams : ResolveRoute< TRouter, @@ -415,7 +416,7 @@ export type IsRequired< ResolveRelativePath extends infer TPath ? undefined extends TPath ? never - : TPath extends CatchAllPaths> + : TPath extends CatchAllPaths ? never : IsRequiredParams< ResolveRelativeToParams diff --git a/packages/react-router/src/routeInfo.ts b/packages/react-router/src/routeInfo.ts index bcb74c9cfd..6c9249e091 100644 --- a/packages/react-router/src/routeInfo.ts +++ b/packages/react-router/src/routeInfo.ts @@ -59,25 +59,30 @@ export type RouteIds = ? CodeRouteIds : InferFileRouteTypes['id'] -export type ParentPath = 'always' extends TOption - ? '../' - : 'never' extends TOption - ? '..' - : '../' | '..' - -export type CurrentPath = 'always' extends TOption - ? './' - : 'never' extends TOption - ? '.' - : './' | '.' - -export type ToPath = 'always' extends TOption - ? `${TTo}/` - : 'never' extends TOption - ? TTo - : TTo | `${TTo}/` - -export type CatchAllPaths = CurrentPath | ParentPath +export type ParentPath = + TrailingSlashOptionByRouter extends 'always' + ? '../' + : TrailingSlashOptionByRouter extends 'never' + ? '..' + : '../' | '..' + +export type CurrentPath = + TrailingSlashOptionByRouter extends 'always' + ? './' + : TrailingSlashOptionByRouter extends 'never' + ? '.' + : './' | '.' + +export type ToPath = + TrailingSlashOptionByRouter extends 'always' + ? AddTrailingSlash + : TrailingSlashOptionByRouter extends 'never' + ? RemoveTrailingSlashes + : AddTrailingSlash | RemoveTrailingSlashes + +export type CatchAllPaths = + | CurrentPath + | ParentPath export type CodeRoutesByPath = ParseRoute extends infer TRoutes extends AnyRoute diff --git a/packages/react-router/tests/Matches.test-d.tsx b/packages/react-router/tests/Matches.test-d.tsx index 00f8734d86..2116e78416 100644 --- a/packages/react-router/tests/Matches.test-d.tsx +++ b/packages/react-router/tests/Matches.test-d.tsx @@ -155,26 +155,14 @@ test('when matching a route with params', () => { .parameter(0) .toHaveProperty('to') .toEqualTypeOf< - | '/' - | '.' - | '..' - | '/invoices' - | '/invoices/$invoiceId' - | '/comments/$id' - | undefined + '/' | '.' | '..' | '/invoices' | '/invoices/$invoiceId' | '/comments/$id' >() - expectTypeOf(MatchRoute) + expectTypeOf(MatchRoute) .parameter(0) .toHaveProperty('to') .toEqualTypeOf< - | '/' - | '.' - | '..' - | '/invoices' - | '/invoices/$invoiceId' - | '/comments/$id' - | undefined + '/' | '.' | '..' | '/invoices' | '/invoices/$invoiceId' | '/comments/$id' >() expectTypeOf( diff --git a/packages/react-router/tests/link.test-d.tsx b/packages/react-router/tests/link.test-d.tsx index 408958d72c..e11cb0314a 100644 --- a/packages/react-router/tests/link.test-d.tsx +++ b/packages/react-router/tests/link.test-d.tsx @@ -175,7 +175,6 @@ test('when navigating to the root', () => { | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(DefaultRouterObjectsLink) @@ -193,7 +192,6 @@ test('when navigating to the root', () => { | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(RouterAlwaysTrailingSlashLink) @@ -211,7 +209,6 @@ test('when navigating to the root', () => { | '/invoices/$invoiceId/edit/' | '/posts/' | '/posts/$postId/' - | undefined >() expectTypeOf(RouterNeverTrailingSlashLink) @@ -229,7 +226,6 @@ test('when navigating to the root', () => { | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(RouterPreserveTrailingSlashLink) @@ -257,7 +253,6 @@ test('when navigating to the root', () => { | '/posts/' | '/posts/$postId' | '/posts/$postId/' - | undefined >() expectTypeOf(DefaultRouterLink) @@ -828,7 +823,6 @@ test('cannot navigate to a branch with an index', () => { | '/invoices/$invoiceId/details/$detailId/lines' | '.' | '..' - | undefined >() expectTypeOf(Link) @@ -846,7 +840,6 @@ test('cannot navigate to a branch with an index', () => { | '/invoices/$invoiceId/details/$detailId/lines' | '.' | '..' - | undefined >() expectTypeOf( @@ -866,7 +859,6 @@ test('cannot navigate to a branch with an index', () => { | '/invoices/$invoiceId/details/$detailId/lines/' | './' | '../' - | undefined >() expectTypeOf(Link) @@ -884,7 +876,6 @@ test('cannot navigate to a branch with an index', () => { | '/invoices/$invoiceId/details/$detailId/lines' | '.' | '..' - | undefined >() expectTypeOf( @@ -914,7 +905,6 @@ test('cannot navigate to a branch with an index', () => { | '..' | './' | '../' - | undefined >() }) @@ -3553,7 +3543,7 @@ test('when navigating to a route with SearchSchemaInput', () => { test('when passing a component with props to createLink and navigating to the root', () => { const MyLink = createLink((props: { additionalProps: number }) => ( - + )) const DefaultRouterLink = MyLink @@ -3589,7 +3579,6 @@ test('when passing a component with props to createLink and navigating to the ro | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(DefaultRouterObjectsLink) @@ -3607,7 +3596,6 @@ test('when passing a component with props to createLink and navigating to the ro | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(RouterAlwaysTrailingSlashLink) @@ -3625,7 +3613,6 @@ test('when passing a component with props to createLink and navigating to the ro | '/invoices/$invoiceId/edit/' | '/posts/' | '/posts/$postId/' - | undefined >() expectTypeOf(RouterNeverTrailingSlashLink) @@ -3643,7 +3630,6 @@ test('when passing a component with props to createLink and navigating to the ro | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(RouterPreserveTrailingSlashLink) @@ -3671,7 +3657,6 @@ test('when passing a component with props to createLink and navigating to the ro | '/posts/' | '/posts/$postId' | '/posts/$postId/' - | undefined >() expectTypeOf(DefaultRouterLink) @@ -4097,7 +4082,6 @@ test('linkOptions', () => { | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(defaultRouterObjectsLinkOptions) @@ -4115,7 +4099,6 @@ test('linkOptions', () => { | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(routerAlwaysTrailingSlashLinkOptions) @@ -4133,7 +4116,6 @@ test('linkOptions', () => { | '/invoices/$invoiceId/edit/' | '/posts/' | '/posts/$postId/' - | undefined >() expectTypeOf(routerNeverTrailingSlashLinkOptions) @@ -4151,7 +4133,6 @@ test('linkOptions', () => { | '/invoices/$invoiceId/edit' | '/posts' | '/posts/$postId' - | undefined >() expectTypeOf(routerPreserveTrailingSlashLinkOptions) @@ -4179,7 +4160,6 @@ test('linkOptions', () => { | '/posts/' | '/posts/$postId' | '/posts/$postId/' - | undefined >() expectTypeOf(defaultRouterLinkOptions) diff --git a/packages/react-router/tests/redirects.test-d.tsx b/packages/react-router/tests/redirects.test-d.tsx index 2516f14ec0..66fc8f7b11 100644 --- a/packages/react-router/tests/redirects.test-d.tsx +++ b/packages/react-router/tests/redirects.test-d.tsx @@ -39,7 +39,5 @@ test('can redirect to valid route', () => { expectTypeOf(redirect) .parameter(0) .toHaveProperty('to') - .toEqualTypeOf< - '/' | '/invoices' | '/invoices/$invoiceId' | '.' | '..' | undefined - >() + .toEqualTypeOf<'/' | '/invoices' | '/invoices/$invoiceId' | '.' | '..'>() }) diff --git a/packages/react-router/tests/router.test-d.tsx b/packages/react-router/tests/router.test-d.tsx index 85582f4555..3e52dbb5ff 100644 --- a/packages/react-router/tests/router.test-d.tsx +++ b/packages/react-router/tests/router.test-d.tsx @@ -56,7 +56,7 @@ test('when navigating using router', () => { expectTypeOf(router.navigate) .parameter(0) .toHaveProperty('to') - .toEqualTypeOf<'/posts' | '/' | '.' | '..' | undefined>() + .toEqualTypeOf<'/posts' | '/' | '.' | '..'>() expectTypeOf(router.navigate) .parameter(0) @@ -90,7 +90,7 @@ test('when building location using router', () => { expectTypeOf(router.buildLocation) .parameter(0) .toHaveProperty('to') - .toEqualTypeOf<'/posts' | '/' | '.' | '..' | undefined>() + .toEqualTypeOf<'/posts' | '/' | '.' | '..'>() expectTypeOf(router.buildLocation) .parameter(0) diff --git a/packages/react-router/tests/useNavigate.test-d.tsx b/packages/react-router/tests/useNavigate.test-d.tsx index 7c19c09887..6fe9f71cac 100644 --- a/packages/react-router/tests/useNavigate.test-d.tsx +++ b/packages/react-router/tests/useNavigate.test-d.tsx @@ -41,9 +41,7 @@ test('when navigating to a route', () => { expectTypeOf(navigate) .parameter(0) .toHaveProperty('to') - .toEqualTypeOf< - '/' | '/invoices' | '/invoices/$invoiceId' | '.' | '..' | undefined - >() + .toEqualTypeOf<'/' | '/invoices' | '/invoices/$invoiceId' | '.' | '..'>() }) test('when setting a default from', () => { diff --git a/packages/router-generator/tests/generator/nested/tests.test-d.ts b/packages/router-generator/tests/generator/nested/tests.test-d.ts index 5363e79ec5..a349a48c5e 100644 --- a/packages/router-generator/tests/generator/nested/tests.test-d.ts +++ b/packages/router-generator/tests/generator/nested/tests.test-d.ts @@ -153,7 +153,6 @@ test('when navigating a index route with search and params', () => { | '/blog/stats' | '/posts/$postId/deep' | '/posts/$postId' - | undefined >() expectTypeOf( @@ -171,7 +170,6 @@ test('when navigating a index route with search and params', () => { | '/blog/stats/' | '/posts/$postId/deep/' | '/posts/$postId/' - | undefined >() expectTypeOf(Link) @@ -188,7 +186,6 @@ test('when navigating a index route with search and params', () => { | '/posts/$postId' | '.' | '..' - | undefined >() expectTypeOf( @@ -214,7 +211,6 @@ test('when navigating a index route with search and params', () => { | '/blog/stats/' | '/posts/$postId/deep/' | '/posts/$postId/' - | undefined >() expectTypeOf(Link) @@ -362,7 +358,6 @@ test('when using useNavigate', () => { | '/blog/stats' | '/posts/$postId/deep' | '/posts/$postId' - | undefined >() })