Skip to content

Commit a59e927

Browse files
committed
fix: improve relative routing types
1 parent 03839c3 commit a59e927

File tree

4 files changed

+114
-83
lines changed

4 files changed

+114
-83
lines changed

packages/react-router/src/link.tsx

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -59,36 +59,36 @@ export type ParsePathParams<T extends string, TAcc = never> = T &
5959
: never
6060
: TAcc
6161

62-
export type AddTrailingSlash<T> = T & `${string}/` extends never
63-
? `${T & string}/`
64-
: T
62+
export type AddTrailingSlash<T> = T extends `${string}/` ? T : `${T & string}/`
6563

66-
export type RemoveTrailingSlashes<T> = T & `${string}/` extends never
67-
? T
68-
: T extends `${infer R}/`
64+
export type RemoveTrailingSlashes<T> = T extends `${string}/`
65+
? T extends `${infer R}/`
6966
? R
7067
: T
71-
72-
export type AddLeadingSlash<T> = T & `/${string}` extends never
73-
? `/${T & string}`
7468
: T
7569

76-
export type RemoveLeadingSlashes<T> = T & `/${string}` extends never
77-
? T
78-
: T extends `/${infer R}`
70+
export type AddLeadingSlash<T> = T extends `/${string}` ? T : `/${T & string}`
71+
72+
export type RemoveLeadingSlashes<T> = T extends `/${string}`
73+
? T extends `/${infer R}`
7974
? R
8075
: T
76+
: T
8177

8278
export type FindDescendantPaths<
8379
TRouter extends AnyRouter,
8480
TPrefix extends string,
85-
> = `${TPrefix}${string}` & RouteToPath<TRouter>
81+
> = (TPrefix | `${TPrefix}/${string}`) & RouteToPath<TRouter>
8682

8783
export type SearchPaths<
8884
TRouter extends AnyRouter,
8985
TPrefix extends string,
9086
TPaths = FindDescendantPaths<TRouter, TPrefix>,
91-
> = TPaths extends `${TPrefix}${infer TRest}` ? TRest : never
87+
> = TPaths extends TPrefix
88+
? ''
89+
: TPaths extends `${TPrefix}/${infer TRest}`
90+
? `/${TRest}`
91+
: never
9292

9393
export type SearchRelativePathAutoComplete<
9494
TRouter extends AnyRouter,
@@ -105,9 +105,12 @@ export type RelativeToParentPathAutoComplete<
105105
>,
106106
> =
107107
| SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
108-
| (TResolvedPath extends ''
109-
? never
110-
: `${TTo}/${ParentPath<TrailingSlashOptionByRouter<TRouter>>}`)
108+
| (TTo extends `${string}..` | `${string}../`
109+
? FindDescendantPaths<TRouter, TResolvedPath> &
110+
TResolvedPath extends TResolvedPath
111+
? never
112+
: `${TTo}/${ParentPath<TrailingSlashOptionByRouter<TRouter>>}`
113+
: never)
111114

112115
export type RelativeToCurrentPathAutoComplete<
113116
TRouter extends AnyRouter,
@@ -141,7 +144,9 @@ export type AbsolutePathAutoComplete<
141144
? never
142145
: string extends TFrom
143146
? never
144-
: RemoveLeadingSlashes<SearchPaths<TRouter, TFrom>>)
147+
: RemoveLeadingSlashes<
148+
SearchPaths<TRouter, RemoveTrailingSlashes<TFrom>>
149+
>)
145150

146151
export type RelativeToPathAutoComplete<
147152
TRouter extends AnyRouter,
@@ -485,46 +490,55 @@ type RemoveLastSegment<
485490
TAcc extends string = '',
486491
> = T extends `${infer TSegment}/${infer TRest}`
487492
? TRest & `${string}/${string}` extends never
488-
? `${TAcc}${TSegment}`
493+
? TRest extends ''
494+
? TAcc
495+
: `${TAcc}${TSegment}`
489496
: RemoveLastSegment<TRest, `${TAcc}${TSegment}/`>
490497
: TAcc
491498

492-
export type ResolveCurrentPath<TFrom, TTo> = TTo extends '.'
499+
export type ResolveCurrentPath<
500+
TFrom extends string,
501+
TTo extends string,
502+
> = TTo extends '.'
493503
? TFrom
494504
: TTo extends './'
495505
? AddTrailingSlash<TFrom>
496506
: TTo & `./${string}` extends never
497507
? never
498508
: TTo extends `./${infer TRest}`
499-
? ResolveRelativePath<TFrom, TRest>
509+
? AddLeadingSlash<JoinPath<TFrom, TRest>>
500510
: never
501511

502-
export type ResolveParentPath<TFrom extends string, TTo> = TTo extends '../'
503-
? RemoveLastSegment<TFrom>
504-
: TTo & `..${string}` extends never
512+
export type ResolveParentPath<
513+
TFrom extends string,
514+
TTo extends string,
515+
> = TTo extends '../' | '..'
516+
? TFrom extends ''
505517
? never
506-
: TTo extends `..${infer ToRest}`
507-
? ResolveRelativePath<
508-
RemoveLastSegment<TFrom>,
509-
RemoveLeadingSlashes<ToRest>
510-
>
511-
: never
518+
: AddLeadingSlash<RemoveLastSegment<TFrom>>
519+
: TTo & `../${string}` extends never
520+
? AddLeadingSlash<JoinPath<TFrom, TTo>>
521+
: TFrom extends ''
522+
? never
523+
: TTo extends `../${infer ToRest}`
524+
? ResolveParentPath<RemoveLastSegment<TFrom>, ToRest>
525+
: AddLeadingSlash<JoinPath<TFrom, TTo>>
512526

513527
export type ResolveRelativePath<TFrom, TTo = '.'> = string extends TFrom
514528
? TTo
515529
: string extends TTo
516530
? TFrom
517531
: undefined extends TTo
518532
? TFrom
519-
: TFrom extends string
520-
? TTo extends string
521-
? TTo & `..${string}` extends never
522-
? TTo & `.${string}` extends never
523-
? TTo & `/${string}` extends never
524-
? AddLeadingSlash<JoinPath<TFrom, TTo>>
525-
: TTo
526-
: ResolveCurrentPath<TFrom, TTo>
527-
: ResolveParentPath<TFrom, TTo>
533+
: TTo extends string
534+
? TFrom extends string
535+
? TTo extends `/${string}`
536+
? TTo
537+
: TTo extends `..${string}`
538+
? ResolveParentPath<TFrom, TTo>
539+
: TTo extends `.${string}`
540+
? ResolveCurrentPath<TFrom, TTo>
541+
: AddLeadingSlash<JoinPath<TFrom, TTo>>
528542
: never
529543
: never
530544

packages/router-generator/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,8 @@
6868
"tsx": "^4.19.2",
6969
"prettier": "^3.4.2",
7070
"zod": "^3.23.8"
71+
},
72+
"devDependencies": {
73+
"@tanstack/react-router": "workspace:^"
7174
}
7275
}

packages/router-generator/tests/generator/nested/tests.test-d.ts

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -383,152 +383,162 @@ test('when using redirect', () => {
383383
})
384384

385385
test('when using useSearch from a route with no search', () => {
386-
expectTypeOf(useSearch<DefaultRouter['routeTree'], '/blog'>)
386+
expectTypeOf(useSearch<DefaultRouter, '/blog'>)
387387
.parameter(0)
388388
.toHaveProperty('from')
389389
.toEqualTypeOf<
390+
| '__root__'
390391
| '/'
391392
| '/blog'
393+
| '/blog/'
392394
| '/posts'
393395
| '/blog/$slug'
394-
| '/blog/stats'
396+
| '/blog_/stats'
395397
| '/blog/'
396398
| '/posts/'
397399
| '/posts/$postId/deep'
398400
| '/posts/$postId/'
399401
>()
400402

401-
expectTypeOf(
402-
useSearch<DefaultRouter['routeTree'], '/blog'>,
403-
).returns.toEqualTypeOf<{}>()
403+
expectTypeOf(useSearch<DefaultRouter, '/blog'>).returns.toEqualTypeOf<{}>()
404404
})
405405

406406
test('when using useSearch from a route with search', () => {
407-
expectTypeOf(useSearch<DefaultRouter['routeTree'], '/blog'>)
407+
expectTypeOf(useSearch<DefaultRouter, '/blog'>)
408408
.parameter(0)
409409
.toHaveProperty('from')
410410
.toEqualTypeOf<
411+
| '__root__'
411412
| '/'
412413
| '/blog'
414+
| '/blog/'
413415
| '/posts'
414416
| '/blog/$slug'
415-
| '/blog/stats'
417+
| '/blog_/stats'
416418
| '/blog/'
417419
| '/posts/'
418420
| '/posts/$postId/deep'
419421
| '/posts/$postId/'
420422
>()
421423

422424
expectTypeOf(
423-
useSearch<DefaultRouter['routeTree'], '/posts/$postId/'>,
425+
useSearch<DefaultRouter, '/posts/$postId/'>,
424426
).returns.toEqualTypeOf<{ indexSearch: string }>()
425427
})
426428

427429
test('when using useLoaderData from a route with loaderData', () => {
428-
expectTypeOf(useLoaderData<DefaultRouter['routeTree'], '/posts/$postId/deep'>)
430+
expectTypeOf(useLoaderData<DefaultRouter, '/posts/$postId/deep'>)
429431
.parameter(0)
430432
.toHaveProperty('from')
431433
.toEqualTypeOf<
434+
| '__root__'
432435
| '/'
433436
| '/blog'
437+
| '/blog/'
434438
| '/posts'
435439
| '/blog/$slug'
436-
| '/blog/stats'
440+
| '/blog_/stats'
437441
| '/blog/'
438442
| '/posts/'
439443
| '/posts/$postId/deep'
440444
| '/posts/$postId/'
441445
>()
442446

443447
expectTypeOf(
444-
useLoaderData<DefaultRouter['routeTree'], '/posts/$postId/deep'>,
448+
useLoaderData<DefaultRouter, '/posts/$postId/deep'>,
445449
).returns.toEqualTypeOf<{ data: string }>()
446450
})
447451

448452
test('when using useLoaderDeps from a route with loaderDeps', () => {
449-
expectTypeOf(useLoaderDeps<DefaultRouter['routeTree'], '/posts/$postId/deep'>)
453+
expectTypeOf(useLoaderDeps<DefaultRouter, '/posts/$postId/deep'>)
450454
.parameter(0)
451455
.toHaveProperty('from')
452456
.toEqualTypeOf<
457+
| '__root__'
453458
| '/'
454459
| '/blog'
460+
| '/blog/'
455461
| '/posts'
456462
| '/blog/$slug'
457-
| '/blog/stats'
463+
| '/blog_/stats'
458464
| '/blog/'
459465
| '/posts/'
460466
| '/posts/$postId/deep'
461467
| '/posts/$postId/'
462468
>()
463469

464470
expectTypeOf(
465-
useLoaderDeps<DefaultRouter['routeTree'], '/posts/$postId/deep'>,
471+
useLoaderDeps<DefaultRouter, '/posts/$postId/deep'>,
466472
).returns.toEqualTypeOf<{ dep: number }>()
467473
})
468474

469475
test('when using useMatch from a route', () => {
470-
expectTypeOf(useMatch<DefaultRouter['routeTree'], '/posts/$postId/deep'>)
476+
expectTypeOf(useMatch<DefaultRouter, '/posts/$postId/deep'>)
471477
.parameter(0)
472478
.toHaveProperty('from')
473479
.toEqualTypeOf<
480+
| '__root__'
474481
| '/'
475482
| '/blog'
483+
| '/blog/'
476484
| '/posts'
477485
| '/blog/$slug'
478-
| '/blog/stats'
486+
| '/blog_/stats'
479487
| '/blog/'
480488
| '/posts/'
481489
| '/posts/$postId/deep'
482490
| '/posts/$postId/'
483491
>()
484492

485493
expectTypeOf(
486-
useMatch<DefaultRouter['routeTree'], '/posts/$postId/deep'>,
494+
useMatch<DefaultRouter, '/posts/$postId/deep'>,
487495
).returns.toEqualTypeOf<
488496
MakeRouteMatch<DefaultRouter['routeTree'], '/posts/$postId/deep', true>
489497
>()
490498
})
491499

492500
test('when using useParams from a route', () => {
493-
expectTypeOf(useParams<DefaultRouter['routeTree'], '/posts/$postId/deep'>)
501+
expectTypeOf(useParams<DefaultRouter, '/posts/$postId/deep'>)
494502
.parameter(0)
495503
.toHaveProperty('from')
496504
.toEqualTypeOf<
505+
| '__root__'
497506
| '/'
498507
| '/blog'
508+
| '/blog/'
499509
| '/posts'
500510
| '/blog/$slug'
501-
| '/blog/stats'
511+
| '/blog_/stats'
502512
| '/blog/'
503513
| '/posts/'
504514
| '/posts/$postId/deep'
505515
| '/posts/$postId/'
506516
>()
507517

508518
expectTypeOf(
509-
useParams<DefaultRouter['routeTree'], '/posts/$postId/deep'>,
519+
useParams<DefaultRouter, '/posts/$postId/deep'>,
510520
).returns.toEqualTypeOf<{ postId: string }>()
511521
})
512522

513523
test('when using useRouteContext from a route', () => {
514-
expectTypeOf(
515-
useRouteContext<DefaultRouter['routeTree'], '/posts/$postId/deep'>,
516-
)
524+
expectTypeOf(useRouteContext<DefaultRouter, '/posts/$postId/deep'>)
517525
.parameter(0)
518526
.toHaveProperty('from')
519527
.toEqualTypeOf<
528+
| '__root__'
520529
| '/'
521530
| '/blog'
531+
| '/blog/'
522532
| '/posts'
523533
| '/blog/$slug'
524-
| '/blog/stats'
534+
| '/blog_/stats'
525535
| '/blog/'
526536
| '/posts/'
527537
| '/posts/$postId/deep'
528538
| '/posts/$postId/'
529539
>()
530540

531541
expectTypeOf(
532-
useRouteContext<DefaultRouter['routeTree'], '/posts/$postId/deep'>,
542+
useRouteContext<DefaultRouter, '/posts/$postId/deep'>,
533543
).returns.toEqualTypeOf<{ someContext: string }>()
534544
})

0 commit comments

Comments
 (0)