@@ -141,12 +141,125 @@ function generateLayoutSlotMap(routesManifest: RouteTypesManifest): string {
141
141
return slotMap
142
142
}
143
143
144
+ function generateRouteValidationTypes (
145
+ routesManifest : RouteTypesManifest
146
+ ) : string {
147
+ const allRoutes = {
148
+ ...routesManifest . appRoutes ,
149
+ ...routesManifest . pageRoutes ,
150
+ ...routesManifest . layoutRoutes ,
151
+ }
152
+
153
+ // Separate static and dynamic routes
154
+ const staticRoutes : string [ ] = [ ]
155
+ const dynamicRoutes : string [ ] = [ ]
156
+
157
+ for ( const [ route , routeInfo ] of Object . entries ( allRoutes ) ) {
158
+ const { groups } = routeInfo
159
+ const hasParams = Object . keys ( groups ) . length > 0
160
+
161
+ if ( hasParams ) {
162
+ dynamicRoutes . push ( route )
163
+ } else {
164
+ staticRoutes . push ( route )
165
+ }
166
+ }
167
+
168
+ // Sort routes for consistent output
169
+ staticRoutes . sort ( )
170
+ dynamicRoutes . sort ( )
171
+
172
+ let result = `// Template literal types for route validation
173
+ type SearchOrHash = \`?\${string}\` | \`#\${string}\`
174
+ type WithProtocol = \`\${string}:\${string}\`
175
+ type Suffix = '' | SearchOrHash
176
+
177
+ type SafeSlug<S extends string> = S extends \`\${string}/\${string}\`
178
+ ? never
179
+ : S extends \`\${string}\${SearchOrHash}\`
180
+ ? never
181
+ : S extends ''
182
+ ? never
183
+ : S
184
+
185
+ type CatchAllSlug<S extends string> = S extends \`\${string}\${SearchOrHash}\`
186
+ ? never
187
+ : S extends ''
188
+ ? never
189
+ : S
190
+
191
+ type OptionalCatchAllSlug<S extends string> =
192
+ S extends \`\${string}\${SearchOrHash}\` ? never : S
193
+
194
+ `
195
+
196
+ // Generate StaticRoutes
197
+ if ( staticRoutes . length > 0 ) {
198
+ result += `type StaticRoutes = ${ staticRoutes . map ( ( route ) => `\`${ route } \`` ) . join ( ' | ' ) } \n\n`
199
+ } else {
200
+ result += 'type StaticRoutes = never\n\n'
201
+ }
202
+
203
+ // Generate DynamicRoutes template
204
+ if ( dynamicRoutes . length > 0 ) {
205
+ result += 'type DynamicRoutes<T extends string = string> = '
206
+ const dynamicTemplates : string [ ] = [ ]
207
+
208
+ for ( const route of dynamicRoutes ) {
209
+ const routeInfo = allRoutes [ route ]
210
+ const { groups } = routeInfo
211
+
212
+ // Convert route pattern to template literal type
213
+ let template = route
214
+ for ( const [ key , group ] of Object . entries ( groups ) ) {
215
+ if ( group . repeat ) {
216
+ if ( group . optional ) {
217
+ // Optional catch-all: [[...param]]
218
+ template = template . replace (
219
+ `[[...${ key } ]]` ,
220
+ `\${OptionalCatchAllSlug<T>}`
221
+ )
222
+ } else {
223
+ // Catch-all: [...param]
224
+ template = template . replace ( `[...${ key } ]` , `\${CatchAllSlug<T>}` )
225
+ }
226
+ } else {
227
+ if ( group . optional ) {
228
+ // Optional param: [[param]]
229
+ template = template . replace ( `[[${ key } ]]` , `\${SafeSlug<T>}` )
230
+ } else {
231
+ // Regular param: [param]
232
+ template = template . replace ( `[${ key } ]` , `\${SafeSlug<T>}` )
233
+ }
234
+ }
235
+ }
236
+ dynamicTemplates . push ( `\`${ template } \`` )
237
+ }
238
+
239
+ result += `${ dynamicTemplates . join ( ' | ' ) } \n\n`
240
+ } else {
241
+ result += 'type DynamicRoutes<T extends string = string> = never\n\n'
242
+ }
243
+
244
+ result += `type ValidRoute<T extends string = string> =
245
+ | StaticRoutes
246
+ | SearchOrHash
247
+ | WithProtocol
248
+ | \`\${StaticRoutes}\${SearchOrHash}\`
249
+ | (T extends \`\${DynamicRoutes<infer _>}\${Suffix}\` ? T : never)
250
+
251
+ `
252
+
253
+ return result
254
+ }
255
+
144
256
export function generateRouteTypesFile (
145
257
routesManifest : RouteTypesManifest
146
258
) : string {
147
259
const routeTypes = generateRouteTypes ( routesManifest )
148
260
const paramTypes = generateParamTypes ( routesManifest )
149
261
const layoutSlotMap = generateLayoutSlotMap ( routesManifest )
262
+ const routeValidationTypes = generateRouteValidationTypes ( routesManifest )
150
263
151
264
return `// This file is generated automatically by Next.js
152
265
// Do not edit this file manually
@@ -164,6 +277,7 @@ type LayoutChildren<P extends LayoutRoutes> = { children: React.ReactNode } & {
164
277
}
165
278
166
279
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes }
280
+ ${ routeValidationTypes }
167
281
168
282
declare global {
169
283
/**
@@ -208,7 +322,7 @@ type NeedsParams<R extends Routes> =
208
322
keyof ParamsOf<R> extends never ? false : true;
209
323
210
324
/**
211
- * Traditional \`href\` navigation.
325
+ * Traditional \`href\` navigation with route validation .
212
326
*
213
327
* @example
214
328
* \`\`\`tsx
@@ -217,8 +331,8 @@ type NeedsParams<R extends Routes> =
217
331
* <Link href={{ pathname: '/about', query: { tab: 'contact' } }}>About</Link>
218
332
* \`\`\`
219
333
*/
220
- export type LinkPropsWithHref = LinkRestProps & {
221
- href: string | UrlObject;
334
+ export type LinkPropsWithHref<RouteInferType = string> = LinkRestProps & {
335
+ href: ValidRoute<RouteInferType> | UrlObject;
222
336
path?: never;
223
337
params?: never;
224
338
searchParams?: never;
@@ -259,9 +373,9 @@ export type LinkPropsWithPath<T extends Routes> = LinkRestProps &
259
373
href?: never;
260
374
});
261
375
262
- export type LinkProps<RouteType extends Routes = Routes > =
263
- | LinkPropsWithHref
264
- | LinkPropsWithPath<RouteType>;
376
+ export type LinkProps<RouteType extends string = string > =
377
+ | LinkPropsWithHref<RouteType>
378
+ | (RouteType extends Routes ? LinkPropsWithPath<RouteType> : never) ;
265
379
266
380
declare module 'next/link' {
267
381
/**
@@ -276,18 +390,61 @@ declare module 'next/link' {
276
390
*
277
391
* @example
278
392
* \`\`\`tsx
279
- * // href mode
393
+ * // href mode with route validation
280
394
* <Link href="/about">About</Link>
395
+ * <Link href="/blog/my-post">Blog Post</Link>
281
396
*
282
- * // path mode
397
+ * // path mode with typed params
283
398
* <Link path="/blog/[slug]" params={{ slug: 'my-post' }}>
284
399
* Blog
285
400
* </Link>
286
401
* \`\`\`
287
402
*/
288
- export default function Link<RouteType extends Routes = Routes >(
403
+ export default function Link<RouteType extends string = string >(
289
404
props: LinkProps<RouteType> & { children: React.ReactNode }
290
405
): JSX.Element;
291
406
}
407
+
408
+ declare module 'next/navigation' {
409
+ export * from 'next/dist/client/components/navigation.js'
410
+
411
+ import type { NavigateOptions, AppRouterInstance as OriginalAppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
412
+ interface AppRouterInstance extends OriginalAppRouterInstance {
413
+ /**
414
+ * Navigate to the provided href.
415
+ * Pushes a new history entry.
416
+ */
417
+ push<RouteType extends Routes = Routes>(href: ValidRoute<RouteType>, options?: NavigateOptions): void
418
+ /**
419
+ * Navigate to the provided href.
420
+ * Replaces the current history entry.
421
+ */
422
+ replace<RouteType extends Routes = Routes>(href: ValidRoute<RouteType>, options?: NavigateOptions): void
423
+ /**
424
+ * Prefetch the provided href.
425
+ */
426
+ prefetch<RouteType extends Routes = Routes>(href: ValidRoute<RouteType>): void
427
+ }
428
+
429
+ export function useRouter(): AppRouterInstance;
430
+ }
431
+
432
+ declare module 'next/form' {
433
+ import type { FormProps as OriginalFormProps } from 'next/dist/client/form.js'
434
+
435
+ type FormRestProps = Omit<OriginalFormProps, 'action'>
436
+
437
+ export type FormProps<RouteInferType = string> = {
438
+ /**
439
+ * \`action\` can be either a \`string\` or a function.
440
+ * - If \`action\` is a string, it will be interpreted as a path or URL to navigate to when the form is submitted.
441
+ * The path will be prefetched when the form becomes visible.
442
+ * - If \`action\` is a function, it will be called when the form is submitted. See the [React docs](https://react.dev/reference/react-dom/components/form#props) for more.
443
+ */
444
+ action: ValidRoute<RouteInferType> | ((formData: FormData) => void)
445
+ } & FormRestProps
446
+
447
+ export default function Form<RouteType = string>(props: FormProps<RouteType>): JSX.Element
448
+ }
292
449
`
293
450
}
0 commit comments