fenok / react-router-typesafe-routes

Comprehensive and extensible type-safe routes for React Router v6 with first-class support for nested routes and param validation.
MIT License
145 stars 3 forks source link

[Question] Type constraints when passing routes #44

Closed AndrewBoklashko closed 1 year ago

AndrewBoklashko commented 1 year ago

Hello! Thank you for a very useful library, I enjoy using it in my React projects 👍

I am stuck with implementing a specific scenario. The idea is that route prop should only accept routes with id param in it. I pass a proper type to Route.TPathTypes, so that I get TS error when trying to pass a route that does not have id param. But I'm not sure how to figure out a constraint for Route.TPath in the same way.

const idParams = types({ params: { id: string() } }).params;
type IdRoute = Route<string, typeof idParams, any, any, any>;

type Props = {
  route?: IdRoute;
}

const Component = ({ route }: Props) => {
  const path = route.buildPath({})
}

This gives an error as expected. image

But here, I expect typescript to give me an error as well since I don't pass an id here. image

fenok commented 1 year ago

Hello and thank you!

That's an interesting case. I would do it like this:

const idFragment = route(':id', { params: { id: string() } });

// The key is that we need to specify all possible paths
type IdRoute = Route<':id' | 'user/:id', { id: UniversalType<string> }, any, never[], any>;

const userRoute = route('user/:id', types(idFragment));

processRouteWithId(userRoute);

function processRouteWithId(idRoute: IdRoute) {
  console.log(idRoute.buildPath({ id: '1' }));
}

IdRoute has to have pathname params in TPath, because without proper TPath we wouldn't know which params are required. And to make routes assignable to IdRoute, it has to specify all possible paths. It's not ideal, but I think it's the only way, at least for now.

Please also note that Route is not designed for this kind of usage, so it's not very convenient. For example, here I had to specify the hash type as never[], and it will actually break for routes with specified hash values. It's something that will be addressed in the next major version.

fenok commented 1 year ago

Wait, you can actually do it like this:

const idFragment = route(':id', { params: { id: string() } });

type IdRoute = Route<`${string}:id` | `${string}:id/${string}`, { id: UniversalType<string> }, any, never[], any>;

const userRoute = route('user/:id', types(idFragment));
const userSubRoute = route('user/:id/:test?', types(idFragment));

processRouteWithId(userRoute);
processRouteWithId(userSubRoute);

function processRouteWithId(idRoute: IdRoute) {
  console.log(idRoute.buildPath({ id: '1' }));
}
AndrewBoklashko commented 1 year ago

That works, thank you much!

Just for context, this is very helpful in my case because I have an admin panel application with a couple of generic table components that can display different types of data. User can click on a table row to navigate to edit page that corresponds to that row. Depending on what kind of data is rendered in the table I need to navigate a user to an appropriate edit page.

Since we have dozens types of data and even more places where table components are used, I want to make sure that it's as type safe as possible.