X Tutup
Skip to content

Commit 8bb05ba

Browse files
committed
feat: improve route type safety
1 parent 5cbfecc commit 8bb05ba

File tree

8 files changed

+71
-23
lines changed

8 files changed

+71
-23
lines changed

apps/demo-solid/src/components/home.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default function Home() {
7878
</stacklayout>
7979
</gridlayout>
8080

81-
<Link to={`/posts/${featured().id}`} state={featuredStoryTransitionState}>
81+
<Link to="/posts/$postId" params={{ postId: featured().id }} state={featuredStoryTransitionState}>
8282
<gridlayout rows="220, auto" class="rounded-[24] bg-white dark:bg-[#0e1628]" style="overflow: hidden;">
8383
<imagecacheit row="0" src={featured().coverUrl} stretch="aspectFill" />
8484

@@ -126,7 +126,7 @@ export default function Home() {
126126
<label text="Route params + loader detail page" class="text-[#8790a2] dark:text-[#9aa7c7] text-[13px] mt-1 leading-[3]" textWrap={true} />
127127
</stacklayout>
128128
<stacklayout col="1">
129-
<Link to="/posts/2">
129+
<Link to="/posts/$postId" params={{ postId: '2' }}>
130130
<label text="Open" class="bg-[#e8eefc] dark:bg-[#223156] text-[#3458c8] dark:text-[#9db5ff] rounded-full px-4 py-2 text-[14px]" />
131131
</Link>
132132
</stacklayout>
@@ -151,7 +151,7 @@ export default function Home() {
151151
<imagecacheit col="0" src={users()[firstPopular().authorId]?.avatarUrl || author().avatarUrl} class="w-[28] h-[28] rounded-full" stretch="aspectFill" />
152152
<label col="1" text={`${users()[firstPopular().authorId]?.name || author().name}${firstPopular().readMinutes}`} class="text-[#8790a2] dark:text-[#9aa7c7] text-[13px] ml-2 leading-[3]" textWrap={true} />
153153
</gridlayout>
154-
<Link to={`/posts/${firstPopular().id}`} class="mt-3">
154+
<Link to="/posts/$postId" params={{ postId: firstPopular().id }} class="mt-3">
155155
<label text="Read Trending Story" class="text-[#131722] dark:text-[#e5ebff] text-[16px] font-bold bg-[#f4f6fb] dark:bg-[#1a2540] rounded-2xl py-4 text-center" />
156156
</Link>
157157
</stacklayout>

apps/demo-solid/src/components/post-detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default function PostDetail() {
112112

113113
<gridlayout columns="*" rows="auto" class="mt-5">
114114
<stacklayout>
115-
<Link to={`/posts/${data().nextPostId}`}>
115+
<Link to="/posts/$postId" params={{ postId: data().nextPostId }}>
116116
<label
117117
text="Next Story"
118118
class="text-white text-[16px] font-bold bg-[#131722] dark:bg-[#2b3a63] rounded-2xl py-4 text-center"

apps/demo-solid/src/components/post-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default function PostList() {
7878
columnGap={10}
7979
>
8080
<stacklayout col="0">
81-
<Link to={`/posts/${post.id}`}>
81+
<Link to="/posts/$postId" params={{ postId: post.id }}>
8282
<label
8383
text="Read Story"
8484
class="text-white text-[16px] font-bold bg-[#11172a] rounded-[18] py-4 text-center"

packages/tanstack-router/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,32 @@ import { Link } from '@nativescript/tanstack-router/solid'
8787
<Link to="/about">
8888
<label text="Go to About" />
8989
</Link>
90+
91+
// Parameterized route (type-safe)
92+
<Link to="/posts/$postId" params={{ postId: '123' }}>
93+
<label text="Open Post 123" />
94+
</Link>
9095
```
9196

97+
### Type Safety Notes
98+
99+
`Link` route typing is validated against your registered router when you add:
100+
101+
```ts
102+
declare module '@nativescript/tanstack-router/solid' {
103+
interface Register {
104+
router: typeof router
105+
}
106+
}
107+
```
108+
109+
For parameterized routes, prefer route patterns + `params`:
110+
111+
- Use: `to="/posts/$postId"` with `params={{ postId: id }}`
112+
- Avoid: ``to={`/posts/${id}`}``
113+
114+
With strict typing, invalid paths (for example `to="/invalid"`) produce TypeScript errors.
115+
92116
### Link Back Navigation
93117

94118
Use `back` to perform native-style stack pop (history back) instead of pushing to `to`.

packages/tanstack-router/src/Link.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import { createMemo, createRenderEffect, onCleanup, type JSX } from 'solid-js'
22
import { createElement, insert, setProp } from '@nativescript-community/solid-js'
3+
import type { AnyRouter, LinkOptions, RoutePaths } from '@tanstack/solid-router'
34
import { useRouter, useMatchRoute } from '@tanstack/solid-router'
5+
import type { RegisteredRouter } from './register'
46
import { resolveLinkTapAction, type LinkTapResult } from './link-action'
57
import { MODAL_SEARCH_PARAM_KEY, withSingleModalPath } from './modal-state'
68
import { closeModalFromRouterContext } from './modal-controller'
79

8-
interface LinkProps {
9-
to?: string
10-
params?: Record<string, string>
11-
search?: Record<string, unknown> | ((prev: unknown) => unknown)
12-
state?: true | Record<string, unknown> | ((prev: unknown) => unknown)
13-
hash?: string
14-
replace?: boolean
10+
type LinkProps<
11+
TRouter extends AnyRouter = RegisteredRouter,
12+
TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
13+
TTo extends string | undefined = '.',
14+
TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
15+
TMaskTo extends string = '.',
16+
> = Omit<LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>, 'to' | 'state'> & {
17+
to?: LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>['to']
18+
state?: true | object | ((prev: unknown) => unknown)
1519
back?: boolean
1620
closeModal?: boolean
1721
modalTo?: string
18-
fallbackTo?: string
22+
fallbackTo?: LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>['to']
1923
onTap?: () => LinkTapResult
2024
children: JSX.Element
2125
class?: string
@@ -24,9 +28,11 @@ interface LinkProps {
2428
style?: string
2529
}
2630

31+
type AnyLinkProps = LinkProps<AnyRouter, string, string | undefined, string, string>
32+
2733
function resolveNextState(
2834
prev: unknown,
29-
stateInput: LinkProps['state'],
35+
stateInput: AnyLinkProps['state'],
3036
): unknown {
3137
if (stateInput === undefined || stateInput === true) {
3238
return prev
@@ -51,7 +57,7 @@ function resolveNextState(
5157
}
5258
}
5359

54-
function resolveNavigateState(stateInput: LinkProps['state']): true | ((prev: any) => any) | undefined {
60+
function resolveNavigateState(stateInput: AnyLinkProps['state']): true | ((prev: any) => any) | undefined {
5561
if (stateInput === undefined) {
5662
return undefined
5763
}
@@ -63,7 +69,13 @@ function resolveNavigateState(stateInput: LinkProps['state']): true | ((prev: an
6369
return (prev: any) => resolveNextState(prev, stateInput)
6470
}
6571

66-
export function Link(props: LinkProps) {
72+
export function Link<
73+
TRouter extends AnyRouter = RegisteredRouter,
74+
const TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
75+
const TTo extends string | undefined = '.',
76+
const TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
77+
const TMaskTo extends string = '.',
78+
>(props: LinkProps<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) {
6779
const router = useRouter()
6880
const matchRoute = useMatchRoute()
6981

@@ -76,7 +88,7 @@ export function Link(props: LinkProps) {
7688
to: props.to as any,
7789
params: props.params as any,
7890
fuzzy: false,
79-
})
91+
})()
8092
})
8193

8294
const handleTap = () => {
@@ -114,7 +126,7 @@ export function Link(props: LinkProps) {
114126
if (props.modalTo) {
115127
const modalTo = props.modalTo
116128
router.navigate({
117-
to: props.to || '.',
129+
to: (props.to || '.') as TTo,
118130
params: props.params as any,
119131
state: resolveNavigateState(props.state),
120132
hash: props.hash as any,
@@ -134,6 +146,10 @@ export function Link(props: LinkProps) {
134146
return
135147
}
136148

149+
if (typeof action.to !== 'string') {
150+
return
151+
}
152+
137153
router.navigate({
138154
to: action.to,
139155
params: props.params as any,

packages/tanstack-router/src/link-action.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
export type LinkTapResult = void | boolean;
22

3-
export interface ResolveLinkTapActionOptions {
3+
export interface ResolveLinkTapActionOptions<TTo extends string | {} = string> {
44
onTapResult?: LinkTapResult;
55
back?: boolean;
66
closeModal?: boolean;
77
canGoBack: boolean;
8-
fallbackTo?: string;
9-
to?: string;
8+
fallbackTo?: TTo;
9+
to?: TTo;
1010
}
1111

12-
export type LinkTapAction = { type: 'none' } | { type: 'back' } | { type: 'close_modal' } | { type: 'navigate'; to: string };
12+
export type LinkTapAction<TTo extends string | {} = string> = { type: 'none' } | { type: 'back' } | { type: 'close_modal' } | { type: 'navigate'; to: TTo };
1313

14-
export function resolveLinkTapAction(opts: ResolveLinkTapActionOptions): LinkTapAction {
14+
export function resolveLinkTapAction<TTo extends string | {} = string>(opts: ResolveLinkTapActionOptions<TTo>): LinkTapAction<TTo> {
1515
if (opts.onTapResult === false) {
1616
return { type: 'none' };
1717
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { RegisteredRouter as CoreRegisteredRouter } from '@tanstack/router-core';
2+
3+
// Module augmentation target for consumers:
4+
// declare module '@nativescript/tanstack-router/solid' { interface Register { router: typeof router } }
5+
export interface Register {}
6+
7+
export type RegisteredRouter = CoreRegisteredRouter<Register>;

packages/tanstack-router/src/solid/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { createNativeScriptHistory } from '../history';
55
export { createNativeScriptRouter } from '../router';
66
export { NativeScriptRouterProvider } from '../NativeScriptRouterProvider';
77
export { Link } from '../Link';
8+
export type { Register, RegisteredRouter } from '../register';
89
export { createNativeScriptNavigationState, createNativeScriptTransitionState } from '../transition-state';
910
export { MODAL_SEARCH_PARAM_KEY, withSingleModalPath } from '../modal-state';
1011
export type { NativeScriptNavigationOptions, NativeScriptNavigationState, NativeScriptNavigationTransition, NativeScriptModalDetent, NativeScriptModalIOSPresentationOptions, NativeScriptModalPresentationOptions, NativeScriptModalOptionsResolver, NativeScriptModalOptionsResolverContext } from '../types';

0 commit comments

Comments
 (0)
X Tutup