-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typesafe useRouteLoaderData behind future flag #12535
base: dev
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: de8f6d7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
5649567
to
cd237e3
Compare
Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳 |
cd237e3
to
1bff006
Compare
I realized that I had accidentally deleted the Now everything should be good! |
I really like this idea |
I have done some more experimenting because there's a hack that I quite disliked in the solution I wrote above. I came up with a more generally-useful approach that could be used in the future, to type any feature that depends on the user's project (and thus can't be known in the react-router repository). It still has the issue of a file being generated in The core improvement of the new technique is generating a file in // .react-router/types/react-router-user-types.d.ts
declare module "react-router/user-types" {
// This is just one way of doing things, and currently references .react-router/types/routeManifest.d.ts as with the implementation above.
// You could just as well include the code from `.react-router/types/routeManifest.d.ts` in this file as well.
export interface UserTypes {
routeManifest: import("./routeManifest").RouteManifest;
routeId: import("./routeManifest").RouteId;
}
} Additionally, for making the future flag work on the type-level too, I'm also generating // .react-router/types/react-router-future.d.ts
declare module "react-router/future" {
export interface TypeLevelFutureFlagConfig {
typesafeUseRouteLoaderData: false; // or true, if the flag is enabled in react-router.config.ts
}
} This allows referencing these types within the react router repository. For // in hooks.tsx
import type { TypeLevelFutureFlagConfig } from "react-router/future";
import type { UserTypes } from "react-router/user-types";
// ...
type UseRouteLoaderData =
TypeLevelFutureFlagConfig["typesafeUseRouteLoaderData"] extends true
// this is the type if the future flag is enabled
? <RI extends UserTypes["routeId"]>(
routeId: RI
) => UserTypes["routeManifest"][RI]["loaderData"]
// this is the type if the future flag is disabled (the default)
: <T = any>(routeId: string) => SerializeFrom<T> | undefined;
export var useRouteLoaderData: UseRouteLoaderData = (routeId) => {
let state = useDataRouterState(DataRouterStateHook.UseRouteLoaderData);
return state.loaderData[routeId];
}; The react-router repo would just need a .d.ts file setting the defaults for a nicer experience during development. Since they aren't included in the user's tsconfig, they are ignored and don't affect the actual user types. I'm sure the naming can be improved, and I'm not sure if the nesting into interfaces provides any advantages (they just resulted from my experimentation). It might also be a good idea to provide empty runtime modules for But I think the concept is pretty clear. This approach could also be used to deal with these issues, by making it possible to detect library vs. framework use on the type level: #12348 #12488 |
enable via typesafeUseRouteLoaderData future flag
1bff006
to
de8f6d7
Compare
This PR implements a new future flag:
typesafeUseRouteLoaderData
, which improves the type foruseRouteLoaderData
by making use of the new type generation infrastructure.Note: I'm not sure if this affects library-mode. If it does, there's probably more work to be done.
Benefits
There are two benefits when enabling the flag:
Benefit 1: Type Inference
Benefit 2: Type Errors
How does it work?
The current useRouteLoaderData type from its actual implementation is made impossible to reach, by requiring the parameter
routeId
to be of typenever
. The actual type is then declared using module augmentation within the user's.react-router/types
directory. The actual generated type depends on the future flag.When typesafeUseRouteLoaderData is disabled
Unfortunately, there is an additional generated file in
.react-router/types
for users who don't enable the flag (.react-router/types/useRouteLoaderData.d.ts
). I'm not sure if that counts as changing the API. I wouldn't say so, but if you disagree I can also understand the reasoning behind that.If the additional generated file counts as an API change, Benefit 2 will require a different approach to be implementable behind a future flag: namely, a type-level future flag via module augmentation. Benefit 1 (inference) can be implemented without that file in case the flag is disabled (if the parameter is not a RouteId, it would then fall back to the current type if nothing is done to enable Benefit 2).
When typesafeUseRouteLoaderData is enabled
react-router typegen
generates two new files in.react-router/types
when the future flag is enabled:.react-router/types/routeManifest.d.ts
exports two types:RouteManifest
andRouteId
.RouteManifest
is an object type, which maps route ids to theInfo
export of that route's+types/...
file. I chose to do this in a separate file, as these types might be useful for users as well..react-router/types/useRouteLoaderData.d.ts
uses module augmentation to declare an new type foruseRouteLoaderData
, which uses theRouteManifest
andRouteId
types for.react-router/types/routeManifest.d.ts
.