@@ -21,38 +21,16 @@ import {
21
21
import { isLocalURL } from '../../shared/lib/router/utils/is-local-url'
22
22
import { dispatchNavigateAction } from '../components/app-router-instance'
23
23
import { errorOnce } from '../../shared/lib/utils/error-once'
24
+ import { constructHref } from '../../shared/lib/router/utils/construct-href'
24
25
25
26
type Url = string | UrlObject
26
- type RequiredKeys < T > = {
27
- [ K in keyof T ] -?: { } extends Pick < T , K > ? never : K
28
- } [ keyof T ]
29
27
type OptionalKeys < T > = {
30
28
[ K in keyof T ] -?: { } extends Pick < T , K > ? K : never
31
29
} [ keyof T ]
32
30
33
31
type OnNavigateEventHandler = ( event : { preventDefault : ( ) => void } ) => void
34
32
35
- type InternalLinkProps = {
36
- /**
37
- * **Required**. The path or URL to navigate to. It can also be an object (similar to `URL`).
38
- *
39
- * @example
40
- * ```tsx
41
- * // Navigate to /dashboard:
42
- * <Link href="/dashboard">Dashboard</Link>
43
- *
44
- * // Navigate to /about?name=test:
45
- * <Link href={{ pathname: '/about', query: { name: 'test' } }}>
46
- * About
47
- * </Link>
48
- * ```
49
- *
50
- * @remarks
51
- * - For external URLs, use a fully qualified URL such as `https://...`.
52
- * - In the App Router, dynamic routes must not include bracketed segments in `href`.
53
- */
54
- href : Url
55
-
33
+ type InternalLinkPropsBase = {
56
34
/**
57
35
* @deprecated v10.0.0: `href` props pointing to a dynamic route are
58
36
* automatically resolved and no longer require the `as` prop.
@@ -212,14 +190,70 @@ type InternalLinkProps = {
212
190
onNavigate ?: OnNavigateEventHandler
213
191
}
214
192
193
+ type InternalLinkProps = InternalLinkPropsBase &
194
+ (
195
+ | {
196
+ /**
197
+ * **Required**. The path or URL to navigate to. It can also be an object (similar to `URL`).
198
+ * Accepts any string for external URLs and backwards compatibility.
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * // Navigate to /dashboard:
203
+ * <Link href="/dashboard">Dashboard</Link>
204
+ *
205
+ * // External URL:
206
+ * <Link href="https://example.com">External Site</Link>
207
+ *
208
+ * // Navigate to /about?name=test:
209
+ * <Link href={{ pathname: '/about', query: { name: 'test' } }}>
210
+ * About
211
+ * </Link>
212
+ * ```
213
+ *
214
+ * @remarks
215
+ * - For external URLs, use a fully qualified URL such as `https://...`.
216
+ * - In the App Router, dynamic routes must not include bracketed segments in `href`.
217
+ */
218
+ href : Url
219
+
220
+ /**
221
+ * These props are not available when using href
222
+ */
223
+ path ?: never
224
+ params ?: never
225
+ searchParams ?: never
226
+ }
227
+ | {
228
+ /**
229
+ * The href property is not available when using path
230
+ */
231
+ href ?: never
232
+
233
+ /**
234
+ * The route path template for typed links (e.g., '/blog/[slug]')
235
+ */
236
+ path : string
237
+
238
+ /**
239
+ * Parameters for dynamic route segments (only available with path)
240
+ */
241
+ params ?: Record < string , string | string [ ] >
242
+
243
+ /**
244
+ * Search parameters to append to the URL (only available with path)
245
+ */
246
+ searchParams ?: Record < string , string | string [ ] >
247
+ }
248
+ )
249
+
215
250
// TODO-APP: Include the full set of Anchor props
216
251
// adding this to the publicly exported type currently breaks existing apps
217
252
218
253
// `RouteInferType` is a stub here to avoid breaking `typedRoutes` when the type
219
254
// isn't generated yet. It will be replaced when the webpack plugin runs.
220
255
// eslint-disable-next-line @typescript-eslint/no-unused-vars
221
256
export type LinkProps < RouteInferType = any > = InternalLinkProps
222
- type LinkPropsRequired = RequiredKeys < LinkProps >
223
257
type LinkPropsOptional = OptionalKeys < Omit < InternalLinkProps , 'locale' > >
224
258
225
259
function isModifiedEvent ( event : React . MouseEvent ) : boolean {
@@ -341,8 +375,11 @@ export default function LinkComponent(
341
375
onNavigate,
342
376
ref : forwardedRef ,
343
377
unstable_dynamicOnHover,
378
+ path,
379
+ params,
380
+ searchParams,
344
381
...restProps
345
- } = props
382
+ } = props as any // TypeScript discriminated union handled at type level
346
383
347
384
children = childrenProp
348
385
@@ -382,31 +419,41 @@ export default function LinkComponent(
382
419
)
383
420
}
384
421
385
- // TypeScript trick for type-guarding:
386
- const requiredPropsGuard : Record < LinkPropsRequired , true > = {
387
- href : true ,
388
- } as const
389
- const requiredProps : LinkPropsRequired [ ] = Object . keys (
390
- requiredPropsGuard
391
- ) as LinkPropsRequired [ ]
392
- requiredProps . forEach ( ( key : LinkPropsRequired ) => {
393
- if ( key === 'href' ) {
394
- if (
395
- props [ key ] == null ||
396
- ( typeof props [ key ] !== 'string' && typeof props [ key ] !== 'object' )
397
- ) {
398
- throw createPropError ( {
399
- key,
400
- expected : '`string` or `object`' ,
401
- actual : props [ key ] === null ? 'null' : typeof props [ key ] ,
402
- } )
403
- }
404
- } else {
405
- // TypeScript trick for type-guarding:
406
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
407
- const _ : never = key
422
+ // Validate that exactly one of href or path is provided
423
+ if ( hrefProp && path ) {
424
+ throw new Error (
425
+ 'Invalid <Link> with both `href` and `path` props. You must use exactly one of these props.'
426
+ )
427
+ }
428
+
429
+ if ( ! hrefProp && ! path ) {
430
+ throw new Error (
431
+ 'Invalid <Link> with neither `href` nor `path` prop. You must provide exactly one of these props.'
432
+ )
433
+ }
434
+
435
+ if ( params && ! path ) {
436
+ throw new Error (
437
+ 'Invalid <Link> with `params` prop but no `path` prop. `params` can only be used with `path`.'
438
+ )
439
+ }
440
+
441
+ if ( searchParams && ! path ) {
442
+ throw new Error (
443
+ 'Invalid <Link> with `searchParams` prop but no `path` prop. `searchParams` can only be used with `path`.'
444
+ )
445
+ }
446
+
447
+ if ( path && ! params && ! searchParams ) {
448
+ // Check if path appears to have dynamic segments
449
+ if ( path . includes ( '[' ) && path . includes ( ']' ) ) {
450
+ console . warn (
451
+ `<Link> with path="${ path } " appears to have dynamic segments but no params were provided. This may result in incorrect URLs.`
452
+ )
408
453
}
409
- } )
454
+ }
455
+
456
+ // Skip the required props validation since we handle it manually above
410
457
411
458
// TypeScript trick for type-guarding:
412
459
const optionalPropsGuard : Record < LinkPropsOptional , true > = {
@@ -422,6 +469,10 @@ export default function LinkComponent(
422
469
onTouchStart : true ,
423
470
legacyBehavior : true ,
424
471
onNavigate : true ,
472
+ href : true ,
473
+ path : true ,
474
+ params : true ,
475
+ searchParams : true ,
425
476
} as const
426
477
const optionalProps : LinkPropsOptional [ ] = Object . keys (
427
478
optionalPropsGuard
@@ -437,6 +488,38 @@ export default function LinkComponent(
437
488
actual : valType ,
438
489
} )
439
490
}
491
+ } else if ( key === 'href' ) {
492
+ if ( props [ key ] && valType !== 'string' && valType !== 'object' ) {
493
+ throw createPropError ( {
494
+ key,
495
+ expected : '`string` or `object`' ,
496
+ actual : valType ,
497
+ } )
498
+ }
499
+ } else if ( key === 'path' ) {
500
+ if ( props [ key ] && valType !== 'string' ) {
501
+ throw createPropError ( {
502
+ key,
503
+ expected : '`string`' ,
504
+ actual : valType ,
505
+ } )
506
+ }
507
+ } else if ( key === 'params' ) {
508
+ if ( props [ key ] && valType !== 'object' ) {
509
+ throw createPropError ( {
510
+ key,
511
+ expected : '`object`' ,
512
+ actual : valType ,
513
+ } )
514
+ }
515
+ } else if ( key === 'searchParams' ) {
516
+ if ( props [ key ] && valType !== 'object' ) {
517
+ throw createPropError ( {
518
+ key,
519
+ expected : '`object`' ,
520
+ actual : valType ,
521
+ } )
522
+ }
440
523
} else if (
441
524
key === 'onClick' ||
442
525
key === 'onMouseEnter' ||
@@ -477,10 +560,6 @@ export default function LinkComponent(
477
560
actual : valType ,
478
561
} )
479
562
}
480
- } else {
481
- // TypeScript trick for type-guarding:
482
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
483
- const _ : never = key
484
563
}
485
564
} )
486
565
}
@@ -517,12 +596,14 @@ export default function LinkComponent(
517
596
}
518
597
519
598
const { href, as } = React . useMemo ( ( ) => {
520
- const resolvedHref = formatStringOrUrl ( hrefProp )
599
+ const resolvedHref = path
600
+ ? constructHref ( path , params , searchParams )
601
+ : formatStringOrUrl ( hrefProp ! )
521
602
return {
522
603
href : resolvedHref ,
523
604
as : asProp ? formatStringOrUrl ( asProp ) : resolvedHref ,
524
605
}
525
- } , [ hrefProp , asProp ] )
606
+ } , [ hrefProp , asProp , path , params , searchParams ] )
526
607
527
608
// This will return the first child, if multiple are provided it will throw an error
528
609
let child : any
0 commit comments