Skip to content

Commit feeb12f

Browse files
committed
fix: resolve validation types circular reference in loaders
Fixes TypeScript error when using validation types in createFileRoute loaders. Replaced Constrain with conditional types to prevent circular references. - Fix ValidateLinkOptions, ValidateNavigateOptions, ValidateRedirectOptions - Add comprehensive regression tests - Maintain backward compatibility
1 parent a8b23bf commit feeb12f

File tree

3 files changed

+227
-56
lines changed

3 files changed

+227
-56
lines changed

packages/react-router/src/typePrimitives.ts

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@ export type ValidateLinkOptions<
1919
TOptions = unknown,
2020
TDefaultFrom extends string = string,
2121
TComp = 'a',
22-
> = Constrain<
23-
TOptions,
24-
LinkComponentProps<
25-
TComp,
26-
TRouter,
27-
InferFrom<TOptions, TDefaultFrom>,
28-
InferTo<TOptions>,
29-
InferMaskFrom<TOptions>,
30-
InferMaskTo<TOptions>
31-
>
22+
> = TOptions extends LinkComponentProps<
23+
TComp,
24+
TRouter,
25+
InferFrom<TOptions, TDefaultFrom>,
26+
InferTo<TOptions>,
27+
InferMaskFrom<TOptions>,
28+
InferMaskTo<TOptions>
3229
>
30+
? TOptions
31+
: LinkComponentProps<
32+
TComp,
33+
TRouter,
34+
InferFrom<TOptions, TDefaultFrom>,
35+
InferTo<TOptions>,
36+
InferMaskFrom<TOptions>,
37+
InferMaskTo<TOptions>
38+
>
3339

3440
/**
3541
* @private
@@ -43,32 +49,44 @@ export type InferStructuralSharing<TOptions> = TOptions extends {
4349
export type ValidateUseSearchOptions<
4450
TOptions,
4551
TRouter extends AnyRouter = RegisteredRouter,
46-
> = Constrain<
47-
TOptions,
48-
UseSearchOptions<
49-
TRouter,
50-
InferFrom<TOptions>,
51-
InferStrict<TOptions>,
52-
InferShouldThrow<TOptions>,
53-
InferSelected<TOptions>,
54-
InferStructuralSharing<TOptions>
55-
>
52+
> = TOptions extends UseSearchOptions<
53+
TRouter,
54+
InferFrom<TOptions>,
55+
InferStrict<TOptions>,
56+
InferShouldThrow<TOptions>,
57+
InferSelected<TOptions>,
58+
InferStructuralSharing<TOptions>
5659
>
60+
? TOptions
61+
: UseSearchOptions<
62+
TRouter,
63+
InferFrom<TOptions>,
64+
InferStrict<TOptions>,
65+
InferShouldThrow<TOptions>,
66+
InferSelected<TOptions>,
67+
InferStructuralSharing<TOptions>
68+
>
5769

5870
export type ValidateUseParamsOptions<
5971
TOptions,
6072
TRouter extends AnyRouter = RegisteredRouter,
61-
> = Constrain<
62-
TOptions,
63-
UseParamsOptions<
64-
TRouter,
65-
InferFrom<TOptions>,
66-
InferStrict<TOptions>,
67-
InferShouldThrow<TOptions>,
68-
InferSelected<TOptions>,
69-
InferSelected<TOptions>
70-
>
73+
> = TOptions extends UseParamsOptions<
74+
TRouter,
75+
InferFrom<TOptions>,
76+
InferStrict<TOptions>,
77+
InferShouldThrow<TOptions>,
78+
InferSelected<TOptions>,
79+
InferSelected<TOptions>
7180
>
81+
? TOptions
82+
: UseParamsOptions<
83+
TRouter,
84+
InferFrom<TOptions>,
85+
InferStrict<TOptions>,
86+
InferShouldThrow<TOptions>,
87+
InferSelected<TOptions>,
88+
InferSelected<TOptions>
89+
>
7290
export type ValidateLinkOptionsArray<
7391
TRouter extends AnyRouter = RegisteredRouter,
7492
TOptions extends ReadonlyArray<any> = ReadonlyArray<unknown>,
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { createFileRoute } from '../src/fileRoute'
3+
import type {
4+
ValidateLinkOptions,
5+
ValidateUseSearchOptions,
6+
ValidateUseParamsOptions
7+
} from '../src/typePrimitives'
8+
import type {
9+
ValidateNavigateOptions,
10+
ValidateRedirectOptions
11+
} from '@tanstack/router-core'
12+
13+
describe('Validation types regression tests', () => {
14+
test('should not cause TypeScript circular reference error in loader return type', () => {
15+
// This test ensures that ValidateLinkOptions can be used in loader return types
16+
// without causing the TypeScript error:
17+
// 'loader' implicitly has return type 'any' because it does not have a return type annotation
18+
19+
const route = createFileRoute('/user/$userId')({
20+
loader: (): { breadcrumbs: ValidateLinkOptions } => {
21+
const breadcrumbs: ValidateLinkOptions = {
22+
to: '/user/$userId',
23+
params: { userId: '123' }
24+
}
25+
26+
return {
27+
breadcrumbs
28+
}
29+
},
30+
component: () => <div>User</div>
31+
})
32+
33+
expect(route).toBeDefined()
34+
})
35+
36+
test('should work with ValidateLinkOptions directly in loader', () => {
37+
const route = createFileRoute('/profile/$userId')({
38+
loader: () => {
39+
const linkOptions: ValidateLinkOptions = {
40+
to: '/profile/$userId',
41+
params: { userId: '456' }
42+
}
43+
44+
return {
45+
navigation: linkOptions
46+
}
47+
},
48+
component: () => <div>Profile</div>
49+
})
50+
51+
expect(route).toBeDefined()
52+
})
53+
54+
test('should work with array of ValidateLinkOptions', () => {
55+
const route = createFileRoute('/dashboard')({
56+
loader: () => {
57+
const breadcrumbs: ValidateLinkOptions[] = [
58+
{ to: '/' },
59+
{ to: '/dashboard' }
60+
]
61+
62+
return {
63+
breadcrumbs
64+
}
65+
},
66+
component: () => <div>Dashboard</div>
67+
})
68+
69+
expect(route).toBeDefined()
70+
})
71+
72+
test('should work with ValidateNavigateOptions in loader', () => {
73+
const route = createFileRoute('/navigate-test')({
74+
loader: () => {
75+
const navOptions: ValidateNavigateOptions = {
76+
to: '/dashboard'
77+
}
78+
79+
return {
80+
navOptions
81+
}
82+
},
83+
component: () => <div>Navigate Test</div>
84+
})
85+
86+
expect(route).toBeDefined()
87+
})
88+
89+
test('should work with ValidateRedirectOptions in loader', () => {
90+
const route = createFileRoute('/redirect-test')({
91+
loader: () => {
92+
const redirectOptions: ValidateRedirectOptions = {
93+
to: '/login'
94+
}
95+
96+
return {
97+
redirectOptions
98+
}
99+
},
100+
component: () => <div>Redirect Test</div>
101+
})
102+
103+
expect(route).toBeDefined()
104+
})
105+
106+
test('should work with ValidateUseSearchOptions in loader', () => {
107+
const route = createFileRoute('/search-test')({
108+
loader: () => {
109+
const searchOptions: ValidateUseSearchOptions<{ from: '/search-test' }> = {
110+
from: '/search-test'
111+
}
112+
113+
return {
114+
searchOptions
115+
}
116+
},
117+
component: () => <div>Search Test</div>
118+
})
119+
120+
expect(route).toBeDefined()
121+
})
122+
123+
test('should work with ValidateUseParamsOptions in loader', () => {
124+
const route = createFileRoute('/params-test/$id')({
125+
loader: () => {
126+
const paramsOptions: ValidateUseParamsOptions<{ from: '/params-test/$id' }> = {
127+
from: '/params-test/$id'
128+
}
129+
130+
return {
131+
paramsOptions
132+
}
133+
},
134+
component: () => <div>Params Test</div>
135+
})
136+
137+
expect(route).toBeDefined()
138+
})
139+
})

packages/router-core/src/typePrimitives.ts

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,21 @@ export type ValidateNavigateOptions<
7575
TRouter extends AnyRouter = RegisteredRouter,
7676
TOptions = unknown,
7777
TDefaultFrom extends string = string,
78-
> = Constrain<
79-
TOptions,
80-
NavigateOptions<
81-
TRouter,
82-
InferFrom<TOptions, TDefaultFrom>,
83-
InferTo<TOptions>,
84-
InferMaskFrom<TOptions>,
85-
InferMaskTo<TOptions>
86-
>
78+
> = TOptions extends NavigateOptions<
79+
TRouter,
80+
InferFrom<TOptions, TDefaultFrom>,
81+
InferTo<TOptions>,
82+
InferMaskFrom<TOptions>,
83+
InferMaskTo<TOptions>
8784
>
85+
? TOptions
86+
: NavigateOptions<
87+
TRouter,
88+
InferFrom<TOptions, TDefaultFrom>,
89+
InferTo<TOptions>,
90+
InferMaskFrom<TOptions>,
91+
InferMaskTo<TOptions>
92+
>
8893

8994
export type ValidateNavigateOptionsArray<
9095
TRouter extends AnyRouter = RegisteredRouter,
@@ -102,16 +107,21 @@ export type ValidateRedirectOptions<
102107
TRouter extends AnyRouter = RegisteredRouter,
103108
TOptions = unknown,
104109
TDefaultFrom extends string = string,
105-
> = Constrain<
106-
TOptions,
107-
RedirectOptions<
108-
TRouter,
109-
InferFrom<TOptions, TDefaultFrom>,
110-
InferTo<TOptions>,
111-
InferMaskFrom<TOptions>,
112-
InferMaskTo<TOptions>
113-
>
110+
> = TOptions extends RedirectOptions<
111+
TRouter,
112+
InferFrom<TOptions, TDefaultFrom>,
113+
InferTo<TOptions>,
114+
InferMaskFrom<TOptions>,
115+
InferMaskTo<TOptions>
114116
>
117+
? TOptions
118+
: RedirectOptions<
119+
TRouter,
120+
InferFrom<TOptions, TDefaultFrom>,
121+
InferTo<TOptions>,
122+
InferMaskFrom<TOptions>,
123+
InferMaskTo<TOptions>
124+
>
115125

116126
export type ValidateRedirectOptionsArray<
117127
TRouter extends AnyRouter = RegisteredRouter,
@@ -170,12 +180,16 @@ export type ValidateUseSearchResult<
170180
export type ValidateUseParamsResult<
171181
TOptions,
172182
TRouter extends AnyRouter = RegisteredRouter,
173-
> = Constrain<
174-
TOptions,
175-
UseParamsResult<
176-
TRouter,
177-
InferFrom<TOptions>,
178-
InferStrict<TOptions>,
179-
InferSelected<TOptions>
180-
>
183+
> = TOptions extends UseParamsResult<
184+
TRouter,
185+
InferFrom<TOptions>,
186+
InferStrict<TOptions>,
187+
InferSelected<TOptions>
181188
>
189+
? TOptions
190+
: UseParamsResult<
191+
TRouter,
192+
InferFrom<TOptions>,
193+
InferStrict<TOptions>,
194+
InferSelected<TOptions>
195+
>

0 commit comments

Comments
 (0)