From f99283a0545fb65c6fc0c88c3a7c1204806f5678 Mon Sep 17 00:00:00 2001 From: 0372hoanghoccode Date: Thu, 22 Jan 2026 08:43:58 +0700 Subject: [PATCH 1/6] fix: TypeScript compatibility with exactOptionalPropertyTypes: true (#14734) --- .../fix-exact-optional-property-types.md | 12 ++++ integration/typegen-test.ts | 60 +++++++++++++++++++ packages/react-router-dev/typegen/generate.ts | 51 ++++++++++++---- packages/react-router/lib/types/internal.ts | 3 +- packages/react-router/lib/types/utils.ts | 26 +++++--- 5 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 .changeset/fix-exact-optional-property-types.md diff --git a/.changeset/fix-exact-optional-property-types.md b/.changeset/fix-exact-optional-property-types.md new file mode 100644 index 0000000000..89083e6f51 --- /dev/null +++ b/.changeset/fix-exact-optional-property-types.md @@ -0,0 +1,12 @@ +--- +"react-router": patch +"@react-router/dev": patch +--- + +Fix TypeScript compatibility with `exactOptionalPropertyTypes: true` + +When `exactOptionalPropertyTypes` is enabled in `tsconfig.json`, TypeScript requires that optional properties explicitly allow `undefined` values. The generated types from `react-router typegen` were causing type errors because the `RouteModule` type constraint didn't explicitly allow `undefined` for optional properties like `action`, `loader`, etc. + +This change updates the `RouteModule` type definition to explicitly include `| undefined` for all optional properties, making it compatible with `exactOptionalPropertyTypes: true`. + +Fixes #14734 diff --git a/integration/typegen-test.ts b/integration/typegen-test.ts index d9532f1e71..4633a75064 100644 --- a/integration/typegen-test.ts +++ b/integration/typegen-test.ts @@ -33,6 +33,66 @@ test.use({ }); test.describe("typegen", () => { + test("exactOptionalPropertyTypes compatibility", async ({ edit, $ }) => { + await edit({ + "tsconfig.json": tsx` + { + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@react-router/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "exactOptionalPropertyTypes": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true + } + } + `, + "app/routes.ts": tsx` + import { type RouteConfig, route } from "@react-router/dev/routes"; + + export default [ + route("products/:id", "routes/product.tsx") + ] satisfies RouteConfig; + `, + "app/routes/product.tsx": tsx` + import type { Expect, Equal } from "../expect-type" + import type { Route } from "./+types/product" + + export function loader({ params }: Route.LoaderArgs) { + type Test = Expect> + return { planet: "world" } + } + + export default function Component({ loaderData }: Route.ComponentProps) { + type Test = Expect> + return

Hello, {loaderData.planet}!

+ } + `, + }); + await $("pnpm typecheck"); + }); + test("basic", async ({ edit, $ }) => { await edit({ "app/routes.ts": tsx` diff --git a/packages/react-router-dev/typegen/generate.ts b/packages/react-router-dev/typegen/generate.ts index a18cd5f198..68039200a3 100644 --- a/packages/react-router-dev/typegen/generate.ts +++ b/packages/react-router-dev/typegen/generate.ts @@ -1,15 +1,40 @@ import ts from "dedent"; +import * as fs from "node:fs"; import * as Path from "pathe"; import * as Pathe from "pathe/utils"; +import * as TS from "typescript"; +import type { RouteManifestEntry } from "../config/routes"; import * as Babel from "../vite/babel"; import type { Context } from "./context"; import * as Params from "./params"; import * as Route from "./route"; -import type { RouteManifestEntry } from "../config/routes"; export type VirtualFile = { filename: string; content: string }; +/** + * Detect if the project has exactOptionalPropertyTypes enabled in tsconfig.json + */ +function hasExactOptionalPropertyTypes(ctx: Context): boolean { + const tsconfigPath = Path.join(ctx.rootDirectory, "tsconfig.json"); + try { + if (!fs.existsSync(tsconfigPath)) return false; + + const configFile = TS.readConfigFile(tsconfigPath, TS.sys.readFile); + if (configFile.error) return false; + + const parsedConfig = TS.parseJsonConfigFileContent( + configFile.config, + TS.sys, + ctx.rootDirectory, + ); + + return parsedConfig.options.exactOptionalPropertyTypes === true; + } catch { + return false; + } +} + export function typesDirectory(ctx: Context) { return Path.join(ctx.rootDirectory, ".react-router/types"); } @@ -180,10 +205,10 @@ function routeFilesType({ t.tsTypeAnnotation( pages ? t.tsUnionType( - Array.from(pages).map((page) => - t.tsLiteralType(t.stringLiteral(page)), - ), - ) + Array.from(pages).map((page) => + t.tsLiteralType(t.stringLiteral(page)), + ), + ) : t.tsNeverKeyword(), ), ), @@ -208,12 +233,12 @@ function routeModulesType(ctx: Context) { t.tsTypeAnnotation( isInAppDirectory(ctx, route.file) ? t.tsTypeQuery( - t.tsImportType( - t.stringLiteral( - `./${Path.relative(ctx.rootDirectory, ctx.config.appDirectory)}/${route.file}`, - ), + t.tsImportType( + t.stringLiteral( + `./${Path.relative(ctx.rootDirectory, ctx.config.appDirectory)}/${route.file}`, ), - ) + ), + ) : t.tsUnknownKeyword(), ), ), @@ -286,13 +311,15 @@ function getRouteAnnotations({ Path.resolve(ctx.config.appDirectory, file), ); + const useStrictOptionals = hasExactOptionalPropertyTypes(ctx); + const content = ts` // Generated by React Router - import type { GetInfo, GetAnnotations } from "react-router/internal"; + import type { GetInfo, GetAnnotations, StrictOptionals } from "react-router/internal"; - type Module = typeof import("${routeImportSource}") + type Module = ${useStrictOptionals ? "StrictOptionals<" : ""}typeof import("${routeImportSource}")${useStrictOptionals ? ">" : ""} type Info = GetInfo<{ file: "${file}", diff --git a/packages/react-router/lib/types/internal.ts b/packages/react-router/lib/types/internal.ts index 87e1104b05..760382f14c 100644 --- a/packages/react-router/lib/types/internal.ts +++ b/packages/react-router/lib/types/internal.ts @@ -1,8 +1,9 @@ export type { GetAnnotations } from "./route-module-annotations"; +export type { StrictOptionals } from "./utils"; import type { Params } from "./params"; import type { RouteFiles } from "./register"; -import type { GetLoaderData, GetActionData } from "./route-data"; +import type { GetActionData, GetLoaderData } from "./route-data"; import type { RouteModule } from "./route-module.ts"; export type GetInfo = diff --git a/packages/react-router/lib/types/utils.ts b/packages/react-router/lib/types/utils.ts index 37d23d0cd0..12687001cf 100644 --- a/packages/react-router/lib/types/utils.ts +++ b/packages/react-router/lib/types/utils.ts @@ -11,16 +11,26 @@ export type Func = (...args: any[]) => unknown; export type Pretty = { [K in keyof T]: T[K] } & {}; +/** + * Normalize optional properties to be compatible with exactOptionalPropertyTypes: true + * Converts properties like `action: (() => void) | undefined` to `action?: () => void` + */ +// prettier-ignore +export type StrictOptionals = Pretty< + & { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } + & { [K in keyof T as undefined extends T[K] ? K : never]?: Exclude } +>; + // Emulates https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#improved-type-inference-for-object-literals export type Normalize = _Normalize, T>; // prettier-ignore type _Normalize = T extends infer U ? - Pretty< - & { [K in Key as K extends keyof U ? undefined extends U[K] ? never : K : never]: K extends keyof U ? U[K] : never } - & { [K in Key as K extends keyof U ? undefined extends U[K] ? K : never : never]?: K extends keyof U ? U[K] : never } - & { [K in Key as K extends keyof U ? never : K]?: undefined} - > + Pretty< + & { [K in Key as K extends keyof U ? undefined extends U[K] ? never : K : never]: K extends keyof U ? U[K] : never } + & { [K in Key as K extends keyof U ? undefined extends U[K] ? K : never : never]?: K extends keyof U ? U[K] : never } + & { [K in Key as K extends keyof U ? never : K]?: undefined } + > : never type UnionKeys = T extends any ? keyof T : never; @@ -28,7 +38,7 @@ type UnionKeys = T extends any ? keyof T : never; // prettier-ignore type __tests = [ Expect, {}>>, - Expect, {a: string}>>, - Expect, {a: string, b?: undefined} | {a?: undefined , b: string}>>, - Expect, {a?: string, b?: undefined} | {a?: undefined , b?: string}>>, + Expect, { a: string }>>, + Expect, { a: string, b?: undefined } | { a?: undefined, b: string }>>, + Expect, { a?: string, b?: undefined } | { a?: undefined, b?: string }>>, ] From db3bd76f77b37491992ee84599f2c717ca1b66f6 Mon Sep 17 00:00:00 2001 From: 0372hoanghoccode Date: Thu, 22 Jan 2026 08:50:20 +0700 Subject: [PATCH 2/6] chore: sign CLA --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index bb3641e68a..58016d699e 100644 --- a/contributors.yml +++ b/contributors.yml @@ -471,3 +471,4 @@ - zeromask1337 - zheng-chuang - zxTomw +- 0372hoanghoccode From 2dc4126be5670db2007877f1a427711c4c9e658d Mon Sep 17 00:00:00 2001 From: 0372hoanghoccode Date: Thu, 22 Jan 2026 21:49:11 +0700 Subject: [PATCH 3/6] fix: use path.posix.join for APP_DIR to fix Windows test failure The test was using path.join() which creates platform-specific paths (backslashes on Windows), then mixing it with path.posix.join(). This caused path.posix.relative() to calculate incorrect relative paths on Windows, expecting "../routes/route.tsx" but getting "../../../routes/route.tsx". Changed to use path.posix.join() consistently for cross-platform compatibility. --- packages/react-router-fs-routes/__tests__/flatRoutes-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts index 964d20e27e..98a4b98e8d 100644 --- a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts +++ b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts @@ -13,7 +13,7 @@ import { } from "../flatRoutes"; import { normalizeSlashes } from "../normalizeSlashes"; -let APP_DIR = path.join("test", "root", "app"); +let APP_DIR = path.posix.join("test", "root", "app"); describe("flatRoutes", () => { describe("creates proper route paths", () => { From 32b483ef3a0ddf5c55dc9eb938ab12b0874d5dc0 Mon Sep 17 00:00:00 2001 From: 0372hoanghoccode Date: Fri, 23 Jan 2026 02:11:44 +0700 Subject: [PATCH 4/6] fix: use path.posix.join consistently in flatRoutes tests for Windows compatibility --- .../__tests__/flatRoutes-test.ts | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts index 98a4b98e8d..65ba0e45ce 100644 --- a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts +++ b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts @@ -86,7 +86,7 @@ describe("flatRoutes", () => { let manifest = flatRoutesUniversal( APP_DIR, - tests.map((t) => path.join(APP_DIR, "routes", t[0] + ".tsx")), + tests.map((t) => path.posix.join(APP_DIR, "routes", t[0] + ".tsx")), ); for (let [input, expected] of tests) { @@ -636,7 +636,7 @@ describe("flatRoutes", () => { let routeManifest = flatRoutesUniversal( APP_DIR, - files.map(([file]) => path.join(APP_DIR, file)), + files.map(([file]) => path.posix.join(APP_DIR, file)), ); let routes = Object.values(routeManifest); @@ -660,8 +660,8 @@ describe("flatRoutes", () => { test("same number of segments and the same dynamic segment index", () => { let testFiles = [ - path.join(APP_DIR, "routes", "_user.$username.tsx"), - path.join(APP_DIR, "routes", "sneakers.$sneakerId.tsx"), + path.posix.join(APP_DIR, "routes", "_user.$username.tsx"), + path.posix.join(APP_DIR, "routes", "sneakers.$sneakerId.tsx"), ]; let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -682,13 +682,13 @@ describe("flatRoutes", () => { test("index files", () => { let testFiles = [ - path.join("routes", "_dashboard._index.tsx"), - path.join("routes", "_landing._index.tsx"), - path.join("routes", "_index.tsx"), + path.posix.join("routes", "_dashboard._index.tsx"), + path.posix.join("routes", "_landing._index.tsx"), + path.posix.join("routes", "_index.tsx"), ]; // route manifest uses the full path - let fullPaths = testFiles.map((file) => path.join(APP_DIR, file)); + let fullPaths = testFiles.map((file) => path.posix.join(APP_DIR, file)); // this is for the expected error message, // which uses the relative path from the app directory internally @@ -706,12 +706,12 @@ describe("flatRoutes", () => { test("folder/route.tsx matching folder.tsx", () => { let testFiles = [ - path.join("routes", "dashboard", "route.tsx"), - path.join("routes", "dashboard.tsx"), + path.posix.join("routes", "dashboard", "route.tsx"), + path.posix.join("routes", "dashboard.tsx"), ]; // route manifest uses the full path - let fullPaths = testFiles.map((file) => path.join(APP_DIR, file)); + let fullPaths = testFiles.map((file) => path.posix.join(APP_DIR, file)); // this is for the expected error message, // which uses the relative path from the app directory internally @@ -732,11 +732,11 @@ describe("flatRoutes", () => { test("pathless layouts should not collide", () => { let testFiles = [ - path.join(APP_DIR, "routes", "_a.tsx"), - path.join(APP_DIR, "routes", "_a._index.tsx"), - path.join(APP_DIR, "routes", "_a.a.tsx"), - path.join(APP_DIR, "routes", "_b.tsx"), - path.join(APP_DIR, "routes", "_b.b.tsx"), + path.posix.join(APP_DIR, "routes", "_a.tsx"), + path.posix.join(APP_DIR, "routes", "_a._index.tsx"), + path.posix.join(APP_DIR, "routes", "_a.a.tsx"), + path.posix.join(APP_DIR, "routes", "_b.tsx"), + path.posix.join(APP_DIR, "routes", "_b.b.tsx"), ]; let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -748,11 +748,11 @@ describe("flatRoutes", () => { // When using folders and route.tsx files testFiles = [ - path.join(APP_DIR, "routes", "_a", "route.tsx"), - path.join(APP_DIR, "routes", "_a._index", "route.tsx"), - path.join(APP_DIR, "routes", "_a.a", "route.tsx"), - path.join(APP_DIR, "routes", "_b", "route.tsx"), - path.join(APP_DIR, "routes", "_b.b", "route.tsx"), + path.posix.join(APP_DIR, "routes", "_a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "_a._index", "route.tsx"), + path.posix.join(APP_DIR, "routes", "_a.a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "_b", "route.tsx"), + path.posix.join(APP_DIR, "routes", "_b.b", "route.tsx"), ]; routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -765,11 +765,11 @@ describe("flatRoutes", () => { test("nested pathless layouts should not collide", () => { let testFiles = [ - path.join(APP_DIR, "routes", "nested._a.tsx"), - path.join(APP_DIR, "routes", "nested._a._index.tsx"), - path.join(APP_DIR, "routes", "nested._a.a.tsx"), - path.join(APP_DIR, "routes", "nested._b.tsx"), - path.join(APP_DIR, "routes", "nested._b.b.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a._index.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.a.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.b.tsx"), ]; let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -781,11 +781,11 @@ describe("flatRoutes", () => { // When using folders and route.tsx files testFiles = [ - path.join(APP_DIR, "routes", "nested._a", "route.tsx"), - path.join(APP_DIR, "routes", "nested._a._index", "route.tsx"), - path.join(APP_DIR, "routes", "nested._a.a", "route.tsx"), - path.join(APP_DIR, "routes", "nested._b", "route.tsx"), - path.join(APP_DIR, "routes", "nested._b.b", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a._index", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.b", "route.tsx"), ]; routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -798,10 +798,10 @@ describe("flatRoutes", () => { test("legit collisions without nested pathless layouts should collide (paths)", () => { let testFiles = [ - path.join(APP_DIR, "routes", "nested._a.tsx"), - path.join(APP_DIR, "routes", "nested._a.a.tsx"), - path.join(APP_DIR, "routes", "nested._b.tsx"), - path.join(APP_DIR, "routes", "nested._b.a.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.a.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.a.tsx"), ]; let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -819,10 +819,10 @@ describe("flatRoutes", () => { // When using folders and route.tsx files consoleError.mockClear(); testFiles = [ - path.join(APP_DIR, "routes", "nested._a", "route.tsx"), - path.join(APP_DIR, "routes", "nested._a.a", "route.tsx"), - path.join(APP_DIR, "routes", "nested._b", "route.tsx"), - path.join(APP_DIR, "routes", "nested._b.a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.a", "route.tsx"), ]; routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -840,10 +840,10 @@ describe("flatRoutes", () => { test("legit collisions without nested pathless layouts should collide (index routes)", () => { let testFiles = [ - path.join(APP_DIR, "routes", "nested._a.tsx"), - path.join(APP_DIR, "routes", "nested._a._index.tsx"), - path.join(APP_DIR, "routes", "nested._b.tsx"), - path.join(APP_DIR, "routes", "nested._b._index.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a._index.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b._index.tsx"), ]; let routeManifest = flatRoutesUniversal(APP_DIR, testFiles); @@ -861,10 +861,10 @@ describe("flatRoutes", () => { // When using folders and route.tsx files consoleError.mockClear(); testFiles = [ - path.join(APP_DIR, "routes", "nested._a", "route.tsx"), - path.join(APP_DIR, "routes", "nested._a._index", "route.tsx"), - path.join(APP_DIR, "routes", "nested._b", "route.tsx"), - path.join(APP_DIR, "routes", "nested._b._index", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._a._index", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b", "route.tsx"), + path.posix.join(APP_DIR, "routes", "nested._b._index", "route.tsx"), ]; routeManifest = flatRoutesUniversal(APP_DIR, testFiles); From 38bcfdd82fba97a63bc0f40512bd57f1dcb28359 Mon Sep 17 00:00:00 2001 From: 0372hoanghoccode Date: Fri, 23 Jan 2026 02:20:04 +0700 Subject: [PATCH 5/6] fix: add | undefined to all optional route type properties for exactOptionalPropertyTypes compatibility --- .../lib/types/route-module-annotations.ts | 26 +++++++++---------- .../react-router/lib/types/route-module.ts | 20 +++++++------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/react-router/lib/types/route-module-annotations.ts b/packages/react-router/lib/types/route-module-annotations.ts index 30ba7c983e..b9edf30781 100644 --- a/packages/react-router/lib/types/route-module-annotations.ts +++ b/packages/react-router/lib/types/route-module-annotations.ts @@ -41,8 +41,8 @@ type MetaMatch = Pretty<{ /** @deprecated Use `MetaMatch.loaderData` instead */ data: GetLoaderData; loaderData: GetLoaderData; - handle?: unknown; - error?: unknown; + handle?: unknown | undefined; + error?: unknown | undefined; }>; // prettier-ignore @@ -75,7 +75,7 @@ type CreateMetaArgs = { | T["loaderData"] | (HasErrorBoundary extends true ? undefined : never); /** Thrown errors that trigger error boundaries will be passed to the meta function. This is useful for generating metadata for error pages. */ - error?: unknown; + error?: unknown | undefined; /** An array of the current {@link https://api.reactrouter.com/v7/interfaces/react_router.UIMatch.html route matches}, including parent route matches. */ matches: MetaMatches; }; @@ -137,15 +137,15 @@ type CreateHydrateFallbackProps< } & (IsServerFirstRoute extends true ? { /** The data returned from the `loader` */ - loaderData?: ServerDataFrom; + loaderData?: ServerDataFrom | undefined; /** The data returned from the `action` following an action submission. */ - actionData?: ServerDataFrom; + actionData?: ServerDataFrom | undefined; } : { /** The data returned from the `loader` or `clientLoader` */ - loaderData?: T["loaderData"]; + loaderData?: T["loaderData"] | undefined; /** The data returned from the `action` or `clientAction` following an action submission. */ - actionData?: T["actionData"]; + actionData?: T["actionData"] | undefined; }); type Match = Pretty<{ @@ -187,13 +187,13 @@ type CreateComponentProps = { /** The data returned from the `loader` */ loaderData: ServerDataFrom; /** The data returned from the `action` following an action submission. */ - actionData?: ServerDataFrom; + actionData?: ServerDataFrom | undefined; } : { /** The data returned from the `loader` or `clientLoader` */ loaderData: T["loaderData"]; /** The data returned from the `action` or `clientAction` following an action submission. */ - actionData?: T["actionData"]; + actionData?: T["actionData"] | undefined; }); type CreateErrorBoundaryProps< @@ -219,15 +219,15 @@ type CreateErrorBoundaryProps< } & (IsServerFirstRoute extends true ? { /** The data returned from the `loader` */ - loaderData?: ServerDataFrom; + loaderData?: ServerDataFrom | undefined; /** The data returned from the `action` following an action submission. */ - actionData?: ServerDataFrom; + actionData?: ServerDataFrom | undefined; } : { /** The data returned from the `loader` or `clientLoader` */ - loaderData?: T["loaderData"]; + loaderData?: T["loaderData"] | undefined; /** The data returned from the `action` or `clientAction` following an action submission. */ - actionData?: T["actionData"]; + actionData?: T["actionData"] | undefined; }); export type GetAnnotations< diff --git a/packages/react-router/lib/types/route-module.ts b/packages/react-router/lib/types/route-module.ts index f2c4b1e35c..680c9baf71 100644 --- a/packages/react-router/lib/types/route-module.ts +++ b/packages/react-router/lib/types/route-module.ts @@ -1,15 +1,15 @@ import type { Func } from "./utils"; export type RouteModule = { - meta?: Func; - links?: Func; - headers?: Func; - loader?: Func; - clientLoader?: Func; - action?: Func; - clientAction?: Func; - HydrateFallback?: Func; - default?: Func; - ErrorBoundary?: Func; + meta?: Func | undefined; + links?: Func | undefined; + headers?: Func | undefined; + loader?: Func | undefined; + clientLoader?: Func | undefined; + action?: Func | undefined; + clientAction?: Func | undefined; + HydrateFallback?: Func | undefined; + default?: Func | undefined; + ErrorBoundary?: Func | undefined; [key: string]: unknown; // allow user-defined exports }; From 08d06ec2d9479426d94a5f98d08fe71b2fc38ed2 Mon Sep 17 00:00:00 2001 From: 0372hoanghoccode Date: Fri, 23 Jan 2026 02:29:09 +0700 Subject: [PATCH 6/6] fix: normalize paths correctly in route-config test for Windows compatibility --- packages/react-router-dev/__tests__/route-config-test.ts | 2 +- .../react-router-fs-routes/__tests__/flatRoutes-test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-router-dev/__tests__/route-config-test.ts b/packages/react-router-dev/__tests__/route-config-test.ts index 8ecc5f03a2..267ef8ba07 100644 --- a/packages/react-router-dev/__tests__/route-config-test.ts +++ b/packages/react-router-dev/__tests__/route-config-test.ts @@ -14,7 +14,7 @@ const cleanPathsForSnapshot = (obj: any): any => JSON.parse( JSON.stringify(obj, (_key, value) => typeof value === "string" && path.isAbsolute(value) - ? normalizePath(value.replace(process.cwd(), "{{CWD}}")) + ? normalizePath(value).replace(normalizePath(process.cwd()), "{{CWD}}") : value, ), ); diff --git a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts index 65ba0e45ce..2047151963 100644 --- a/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts +++ b/packages/react-router-fs-routes/__tests__/flatRoutes-test.ts @@ -7,8 +7,8 @@ import type { RouteManifestEntry } from "../manifest"; import { flatRoutes, flatRoutesUniversal, - getRoutePathConflictErrorMessage, getRouteIdConflictErrorMessage, + getRoutePathConflictErrorMessage, getRouteSegments, } from "../flatRoutes"; import { normalizeSlashes } from "../normalizeSlashes"; @@ -654,7 +654,7 @@ describe("flatRoutes", () => { describe("doesn't warn when there's not a route collision", () => { let consoleError = jest .spyOn(global.console, "error") - .mockImplementation(() => {}); + .mockImplementation(() => { }); afterEach(consoleError.mockReset); @@ -676,7 +676,7 @@ describe("flatRoutes", () => { describe("warns when there's a route collision", () => { let consoleError = jest .spyOn(global.console, "error") - .mockImplementation(() => {}); + .mockImplementation(() => { }); afterEach(consoleError.mockReset);