Skip to content
46 changes: 33 additions & 13 deletions packages/react-router/src/typePrimitives.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {
AnyRouter,
Constrain,
InferFrom,
InferMaskFrom,
InferMaskTo,
Expand All @@ -19,17 +18,24 @@ export type ValidateLinkOptions<
TOptions = unknown,
TDefaultFrom extends string = string,
TComp = 'a',
> = Constrain<
TOptions,
LinkComponentProps<
> =
TOptions extends LinkComponentProps<
TComp,
TRouter,
InferFrom<TOptions, TDefaultFrom>,
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>
>
? TOptions
: LinkComponentProps<
TComp,
TRouter,
InferFrom<TOptions, TDefaultFrom>,
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>

/**
* @private
Expand All @@ -43,32 +49,46 @@ export type InferStructuralSharing<TOptions> = TOptions extends {
export type ValidateUseSearchOptions<
TOptions,
TRouter extends AnyRouter = RegisteredRouter,
> = Constrain<
TOptions,
UseSearchOptions<
> =
TOptions extends UseSearchOptions<
TRouter,
InferFrom<TOptions>,
InferStrict<TOptions>,
InferShouldThrow<TOptions>,
InferSelected<TOptions>,
InferStructuralSharing<TOptions>
>
>
? TOptions
: UseSearchOptions<
TRouter,
InferFrom<TOptions>,
InferStrict<TOptions>,
InferShouldThrow<TOptions>,
InferSelected<TOptions>,
InferStructuralSharing<TOptions>
>

export type ValidateUseParamsOptions<
TOptions,
TRouter extends AnyRouter = RegisteredRouter,
> = Constrain<
TOptions,
UseParamsOptions<
> =
TOptions extends UseParamsOptions<
TRouter,
InferFrom<TOptions>,
InferStrict<TOptions>,
InferShouldThrow<TOptions>,
InferSelected<TOptions>,
InferSelected<TOptions>
>
>
? TOptions
: UseParamsOptions<
TRouter,
InferFrom<TOptions>,
InferStrict<TOptions>,
InferShouldThrow<TOptions>,
InferSelected<TOptions>,
InferSelected<TOptions>
>
export type ValidateLinkOptionsArray<
TRouter extends AnyRouter = RegisteredRouter,
TOptions extends ReadonlyArray<any> = ReadonlyArray<unknown>,
Expand Down
143 changes: 143 additions & 0 deletions packages/react-router/tests/validateLinkOptions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, expect, test } from 'vitest'
import { createFileRoute } from '../src/fileRoute'
import type {
ValidateLinkOptions,
ValidateUseParamsOptions,
ValidateUseSearchOptions,
} from '../src/typePrimitives'
import type {
ValidateNavigateOptions,
ValidateRedirectOptions,
} from '@tanstack/router-core'

describe('Validation types regression tests', () => {
test('should not cause TypeScript circular reference error in loader return type', () => {
// This test ensures that ValidateLinkOptions can be used in loader return types
// without causing the TypeScript error:
// 'loader' implicitly has return type 'any' because it does not have a return type annotation

const route = createFileRoute('/user/$userId')({
loader: (): { breadcrumbs: ValidateLinkOptions } => {
const breadcrumbs: ValidateLinkOptions = {
to: '/user/$userId',
params: { userId: '123' },
}

return {
breadcrumbs,
}
},
component: () => <div>User</div>,
})

expect(route).toBeDefined()
})

test('should work with ValidateLinkOptions directly in loader', () => {
const route = createFileRoute('/profile/$userId')({
loader: () => {
const linkOptions: ValidateLinkOptions = {
to: '/profile/$userId',
params: { userId: '456' },
}

return {
navigation: linkOptions,
}
},
component: () => <div>Profile</div>,
})

expect(route).toBeDefined()
})

test('should work with array of ValidateLinkOptions', () => {
const route = createFileRoute('/dashboard')({
loader: () => {
const breadcrumbs: Array<ValidateLinkOptions> = [
{ to: '/' },
{ to: '/dashboard' },
]

return {
breadcrumbs,
}
},
component: () => <div>Dashboard</div>,
})

expect(route).toBeDefined()
})

test('should work with ValidateNavigateOptions in loader', () => {
const route = createFileRoute('/navigate-test')({
loader: () => {
const navOptions: ValidateNavigateOptions = {
to: '/dashboard',
}

return {
navOptions,
}
},
component: () => <div>Navigate Test</div>,
})

expect(route).toBeDefined()
})

test('should work with ValidateRedirectOptions in loader', () => {
const route = createFileRoute('/redirect-test')({
loader: () => {
const redirectOptions: ValidateRedirectOptions = {
to: '/login',
}

return {
redirectOptions,
}
},
component: () => <div>Redirect Test</div>,
})

expect(route).toBeDefined()
})

test('should work with ValidateUseSearchOptions in loader', () => {
const route = createFileRoute('/search-test')({
loader: () => {
const searchOptions: ValidateUseSearchOptions<{
from: '/search-test'
}> = {
from: '/search-test',
}

return {
searchOptions,
}
},
component: () => <div>Search Test</div>,
})

expect(route).toBeDefined()
})

test('should work with ValidateUseParamsOptions in loader', () => {
const route = createFileRoute('/params-test/$id')({
loader: () => {
const paramsOptions: ValidateUseParamsOptions<{
from: '/params-test/$id'
}> = {
from: '/params-test/$id',
}

return {
paramsOptions,
}
},
component: () => <div>Params Test</div>,
})

expect(route).toBeDefined()
})
})
43 changes: 30 additions & 13 deletions packages/router-core/src/typePrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { RouteIds } from './routeInfo'
import type { AnyRouter, RegisteredRouter } from './router'
import type { UseParamsResult } from './useParams'
import type { UseSearchResult } from './useSearch'
import type { Constrain, ConstrainLiteral } from './utils'
import type { ConstrainLiteral } from './utils'

export type ValidateFromPath<
TRouter extends AnyRouter = RegisteredRouter,
Expand Down Expand Up @@ -75,16 +75,22 @@ export type ValidateNavigateOptions<
TRouter extends AnyRouter = RegisteredRouter,
TOptions = unknown,
TDefaultFrom extends string = string,
> = Constrain<
TOptions,
NavigateOptions<
> =
TOptions extends NavigateOptions<
TRouter,
InferFrom<TOptions, TDefaultFrom>,
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>
>
? TOptions
: NavigateOptions<
TRouter,
InferFrom<TOptions, TDefaultFrom>,
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>

export type ValidateNavigateOptionsArray<
TRouter extends AnyRouter = RegisteredRouter,
Expand All @@ -102,16 +108,22 @@ export type ValidateRedirectOptions<
TRouter extends AnyRouter = RegisteredRouter,
TOptions = unknown,
TDefaultFrom extends string = string,
> = Constrain<
TOptions,
RedirectOptions<
> =
TOptions extends RedirectOptions<
TRouter,
InferFrom<TOptions, TDefaultFrom>,
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>
>
? TOptions
: RedirectOptions<
TRouter,
InferFrom<TOptions, TDefaultFrom>,
InferTo<TOptions>,
InferMaskFrom<TOptions>,
InferMaskTo<TOptions>
>

export type ValidateRedirectOptionsArray<
TRouter extends AnyRouter = RegisteredRouter,
Expand Down Expand Up @@ -170,12 +182,17 @@ export type ValidateUseSearchResult<
export type ValidateUseParamsResult<
TOptions,
TRouter extends AnyRouter = RegisteredRouter,
> = Constrain<
TOptions,
UseParamsResult<
> =
TOptions extends UseParamsResult<
TRouter,
InferFrom<TOptions>,
InferStrict<TOptions>,
InferSelected<TOptions>
>
>
? TOptions
: UseParamsResult<
TRouter,
InferFrom<TOptions>,
InferStrict<TOptions>,
InferSelected<TOptions>
>
Loading