Skip to content

Commit

Permalink
fix: correctly infer from and avoid having to use as const for st…
Browse files Browse the repository at this point in the history
…ring literals (#1005)
  • Loading branch information
schiller-manuel authored Jan 17, 2024
1 parent 42a6d61 commit c448b52
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 57 deletions.
43 changes: 22 additions & 21 deletions packages/react-router/src/Matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import warning from 'tiny-warning'
import { CatchBoundary, ErrorComponent } from './CatchBoundary'
import { useRouter, useRouterState } from './RouterProvider'
import { ResolveRelativePath, ToOptions } from './link'
import { AnyRoute, ReactNode, rootRouteId } from './route'
import { AnyRoute, ReactNode } from './route'
import {
FullSearchSchema,
ParseRoute,
RouteById,
RouteByPath,
RouteIds,
RoutePaths,
} from './routeInfo'
import { RegisteredRouter, RouterState } from './router'
import { NoInfer, StrictOrFrom, pick } from './utils'
import { GetTFrom, NoInfer, StrictOrFrom, pick } from './utils'

export const matchContext = React.createContext<string | undefined>(undefined)

Expand All @@ -37,8 +36,7 @@ export interface RouteMatch<
loaderData?: RouteById<TRouteTree, TRouteId>['types']['loaderData']
routeContext: RouteById<TRouteTree, TRouteId>['types']['routeContext']
context: RouteById<TRouteTree, TRouteId>['types']['allContext']
search: FullSearchSchema<TRouteTree> &
RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
search: RouteById<TRouteTree, TRouteId>['types']['fullSearchSchema']
fetchCount: number
abortController: AbortController
cause: 'preload' | 'enter' | 'stay'
Expand Down Expand Up @@ -293,16 +291,17 @@ export function getRenderedMatches(state: RouterState) {
}

export function useMatch<
TOpts extends StrictOrFrom<TFrom>,
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
TStrict extends boolean = true,
TRouteMatchState = RouteMatch<TRouteTree, TFrom>,
TFromInferred extends RouteIds<TRouteTree> = GetTFrom<TOpts, TRouteTree>,
TRouteMatchState = RouteMatch<TRouteTree, TFromInferred>,
TSelected = TRouteMatchState,
>(
opts: StrictOrFrom<TFrom> & {
opts: TOpts & {
select?: (match: TRouteMatchState) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
): TSelected {
const router = useRouter()
const nearestMatchId = React.useContext(matchContext)

Expand Down Expand Up @@ -378,49 +377,51 @@ export function useParentMatches<T = RouteMatch[]>(opts?: {
}

export function useLoaderDeps<
TOpts extends StrictOrFrom<TFrom>,
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
TStrict extends boolean = true,
TRouteMatch extends RouteMatch<TRouteTree, TFrom> = RouteMatch<
TFromInferred extends RouteIds<TRouteTree> = GetTFrom<TOpts, TRouteTree>,
TRouteMatch extends RouteMatch<TRouteTree, TFromInferred> = RouteMatch<
TRouteTree,
TFrom
TFromInferred
>,
TSelected = Required<TRouteMatch>['loaderDeps'],
>(
opts: StrictOrFrom<TFrom> & {
opts: TOpts & {
select?: (match: TRouteMatch) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
): TSelected {
return useMatch({
...opts,
select: (s) => {
return typeof opts.select === 'function'
? opts.select(s?.loaderDeps)
: s?.loaderDeps
},
})!
})
}

export function useLoaderData<
TOpts extends StrictOrFrom<TFrom>,
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
TStrict extends boolean = true,
TRouteMatch extends RouteMatch<TRouteTree, TFrom> = RouteMatch<
TFromInferred extends RouteIds<TRouteTree> = GetTFrom<TOpts, TRouteTree>,
TRouteMatch extends RouteMatch<TRouteTree, TFromInferred> = RouteMatch<
TRouteTree,
TFrom
TFromInferred
>,
TSelected = Required<TRouteMatch>['loaderData'],
>(
opts: StrictOrFrom<TFrom> & {
opts: TOpts & {
select?: (match: TRouteMatch) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
): TSelected {
return useMatch({
...opts,
select: (s) => {
return typeof opts.select === 'function'
? opts.select(s?.loaderData)
: s?.loaderData
},
})!
})
}
11 changes: 6 additions & 5 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
NoInfer,
NonNullableUpdater,
PickRequired,
StringLiteral,
Updater,
WithoutEmpty,
deepEqual,
Expand Down Expand Up @@ -114,7 +115,7 @@ export type RelativeToPathAutoComplete<

export type NavigateOptions<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RoutePaths<TRouteTree> | string = string,
TFrom extends RoutePaths<TRouteTree> | string = RoutePaths<TRouteTree>,
TTo extends string = '',
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
TMaskTo extends string = '',
Expand All @@ -128,7 +129,7 @@ export type NavigateOptions<

export type ToOptions<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RoutePaths<TRouteTree> | string = string,
TFrom extends RoutePaths<TRouteTree> | string = RoutePaths<TRouteTree>,
TTo extends string = '',
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
TMaskTo extends string = '',
Expand All @@ -138,15 +139,15 @@ export type ToOptions<

export type ToMaskOptions<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TMaskFrom extends RoutePaths<TRouteTree> | string = string,
TMaskFrom extends RoutePaths<TRouteTree> | string = RoutePaths<TRouteTree>,
TMaskTo extends string = '',
> = ToSubOptions<TRouteTree, TMaskFrom, TMaskTo> & {
unmaskOnReload?: boolean
}

export type ToSubOptions<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RoutePaths<TRouteTree> | string = string,
TFrom extends RoutePaths<TRouteTree> | string = RoutePaths<TRouteTree>,
TTo extends string = '',
TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
> = {
Expand All @@ -156,7 +157,7 @@ export type ToSubOptions<
// State to pass to the history stack
state?: true | NonNullableUpdater<HistoryState>
// 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?: TFrom
from?: StringLiteral<TFrom>
// // When using relative route paths, this option forces resolution from the current path, instead of the route API's path or `from` path
} & CheckPath<TRouteTree, NoInfer<TResolved>, {}> &
SearchParamOptions<TRouteTree, TFrom, TTo, TResolved> &
Expand Down
25 changes: 12 additions & 13 deletions packages/react-router/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,41 +485,40 @@ export class RouteApi<
useMatch = <TSelected = TAllContext>(opts?: {
select?: (s: TAllContext) => TSelected
}): TSelected => {
return useMatch({ ...opts, from: this.id }) as any
return useMatch({ select: opts?.select, from: this.id })
}

useRouteContext = <TSelected = TAllContext>(opts?: {
select?: (s: TAllContext) => TSelected
}): TSelected => {
return useMatch({
...opts,
from: this.id,
select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
} as any)
})
}

useSearch = <TSelected = TFullSearchSchema>(opts?: {
select?: (s: TFullSearchSchema) => TSelected
}): TSelected => {
return useSearch({ ...opts, from: this.id } as any)
return useSearch({ ...opts, from: this.id })
}

useParams = <TSelected = TAllParams>(opts?: {
select?: (s: TAllParams) => TSelected
}): TSelected => {
return useParams({ ...opts, from: this.id } as any)
return useParams({ ...opts, from: this.id })
}

useLoaderDeps = <TSelected = TLoaderDeps>(opts?: {
select?: (s: TLoaderDeps) => TSelected
}): TSelected => {
return useLoaderDeps({ ...opts, from: this.id } as any) as any
return useLoaderDeps({ ...opts, from: this.id } as any)
}

useLoaderData = <TSelected = TLoaderData>(opts?: {
select?: (s: TLoaderData) => TSelected
}): TSelected => {
return useLoaderData({ ...opts, from: this.id } as any) as any
return useLoaderData({ ...opts, from: this.id } as any)
}
}

Expand Down Expand Up @@ -810,7 +809,7 @@ export class Route<
useMatch = <TSelected = TAllContext>(opts?: {
select?: (search: TAllContext) => TSelected
}): TSelected => {
return useMatch({ ...opts, from: this.id }) as any
return useMatch({ ...opts, from: this.id })
}

useRouteContext = <TSelected = TAllContext>(opts?: {
Expand All @@ -820,31 +819,31 @@ export class Route<
...opts,
from: this.id,
select: (d: any) => (opts?.select ? opts.select(d.context) : d.context),
} as any)
})
}

useSearch = <TSelected = TFullSearchSchema>(opts?: {
select?: (search: TFullSearchSchema) => TSelected
}): TSelected => {
return useSearch({ ...opts, from: this.id } as any)
return useSearch({ ...opts, from: this.id })
}

useParams = <TSelected = TAllParams>(opts?: {
select?: (search: TAllParams) => TSelected
}): TSelected => {
return useParams({ ...opts, from: this.id } as any)
return useParams({ ...opts, from: this.id })
}

useLoaderDeps = <TSelected = TLoaderDeps>(opts?: {
select?: (s: TLoaderDeps) => TSelected
}): TSelected => {
return useLoaderDeps({ ...opts, from: this.id } as any) as any
return useLoaderDeps({ ...opts, from: this.id } as any)
}

useLoaderData = <TSelected = TLoaderData>(opts?: {
select?: (search: TLoaderData) => TSelected
}): TSelected => {
return useLoaderData({ ...opts, from: this.id } as any) as any
return useLoaderData({ ...opts, from: this.id } as any)
}
}

Expand Down
7 changes: 4 additions & 3 deletions packages/react-router/src/useNavigate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import { LinkOptions, NavigateOptions } from './link'
import { AnyRoute } from './route'
import { RoutePaths } from './routeInfo'
import { RegisteredRouter } from './router'
import { StringLiteral } from './utils'

export function useNavigate<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TDefaultFrom extends RoutePaths<TRouteTree> | string = string,
>(_defaultOpts?: { from?: TDefaultFrom }) {
const { navigate, buildLocation } = useRouter()
TDefaultFrom extends RoutePaths<TRouteTree> | string = RoutePaths<TRouteTree>,
>(_defaultOpts?: { from?: StringLiteral<TDefaultFrom> }) {
const { navigate } = useRouter()

const matchPathname = useMatch({
strict: false,
Expand Down
13 changes: 7 additions & 6 deletions packages/react-router/src/useParams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import { RouteIds, RouteById, AllParams } from './routeInfo'
import { RegisteredRouter } from './router'
import { last } from './utils'
import { useRouterState } from './RouterProvider'
import { StrictOrFrom } from './utils'
import { StrictOrFrom, GetTFrom } from './utils'
import { getRenderedMatches } from './Matches'

export function useParams<
TOpts extends StrictOrFrom<TFrom>,
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
TDefaultSelected = AllParams<TRouteTree> &
RouteById<TRouteTree, TFrom>['types']['allParams'],
TSelected = TDefaultSelected,
TFromInferred = GetTFrom<TOpts, TRouteTree>,
TParams = RouteById<TRouteTree, TFromInferred>['types']['allParams'],
TSelected = TParams,
>(
opts: StrictOrFrom<TFrom> & {
select?: (search: TDefaultSelected) => TSelected
opts: TOpts & {
select?: (params: TParams) => TSelected
},
): TSelected {
return useRouterState({
Expand Down
13 changes: 7 additions & 6 deletions packages/react-router/src/useSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ import { RouteIds, RouteById } from './routeInfo'
import { RegisteredRouter } from './router'
import { RouteMatch } from './Matches'
import { useMatch } from './Matches'
import { StrictOrFrom } from './utils'
import { StrictOrFrom, GetTFrom } from './utils'

export function useSearch<
TOpts extends StrictOrFrom<TFrom>,
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
TStrict extends boolean = true,
TSearch = RouteById<TRouteTree, TFrom>['types']['fullSearchSchema'],
TFromInferred = GetTFrom<TOpts, TRouteTree>,
TSearch = RouteById<TRouteTree, TFromInferred>['types']['fullSearchSchema'],
TSelected = TSearch,
>(
opts: StrictOrFrom<TFrom> & {
opts: TOpts & {
select?: (search: TSearch) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
) : TSelected {
return useMatch({
...(opts as any),
...opts,
select: (match: RouteMatch) => {
return opts?.select ? opts.select(match.search as TSearch) : match.search
},
Expand Down
17 changes: 14 additions & 3 deletions packages/react-router/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,27 +290,38 @@ export function shallow<T>(objA: T, objB: T) {
return true
}

export type StringLiteral<T> = T extends string
? string extends T
? string
: T
: never

export type StrictOrFrom<TFrom> =
| {
from: TFrom
from: StringLiteral<TFrom> | TFrom
strict?: true
}
| {
from?: never
strict: false
}

export type GetTFrom<T, TRouteTree extends AnyRoute> = T extends StrictOrFrom<
infer TFrom extends RouteIds<TRouteTree>
>
? TFrom
: never

export function useRouteContext<
TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
TStrict extends boolean = true,
TRouteContext = RouteById<TRouteTree, TFrom>['types']['allContext'],
TSelected = TRouteContext,
>(
opts: StrictOrFrom<TFrom> & {
select?: (search: TRouteContext) => TSelected
},
): TStrict extends true ? TSelected : TSelected | undefined {
): TSelected {
return useMatch({
...(opts as any),
select: (match: RouteMatch) =>
Expand Down

0 comments on commit c448b52

Please sign in to comment.