Git Product home page Git Product logo

typesafe-routes's Introduction

Typesafe Routes

Enhance your preferred routing library by incorporating powerful path generation including:

  • Path & template rendering
  • Nested, absolute, and relative paths
  • Parameter parsing and serialization
  • Type-safe, customizable, and extendable
  • Also useful with JavaScript

Quick Reference

The complete documentation can be found here.

  • Methods
    • render: renders a path with parameters
    • template: renders a route template
    • parseParams: parses dynamic segments in a path
    • parseQuery: parses parameters in a search query
  • Chainable operators:
    • bind: binds parameters to a route for later rendering
    • from: creates a new route based on a string-based path (i.e. location.path)
    • replace: replaces segments in a path

Installation

Version 11 is currently under development. Please don't use it in production yet. The official release will happen soon. The v10 documentation can be found here.

npm i typesafe-routes@next # or any npm alternatives

How to Contribute

  • leave a star โญ
  • report a bug ๐Ÿž
  • open a pull request ๐Ÿ—๏ธ
  • help others โค๏ธ
  • buy me a coffee โ˜•

Buy Me A Coffee

Roadmap

  • v11 migration guide
  • check for duplicate param names in the route tree
  • context caching
  • customizable parsing of search params
  • demos & utils
    • react-router
    • refinejs
    • vue router
    • angular router

typesafe-routes's People

Contributors

armedi avatar codebutler avatar dependabot[bot] avatar grumd avatar jeysal avatar kruschid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

typesafe-routes's Issues

Matching a route via multiple paths

This is more of a discussion issue at this point, I just encountered something that was a bit awkward to express using this library and react-router and thought maybe we could brainstorm whether there is a pretty solution to this. I'm not actually sure there is something the library can do to help with this, but maybe I'm missing something and someone else has an idea.

The problem

We have a page with three tabs. So far straightforward. However, over each of these tabs, the same semi-opaque modal for editing some information in the page header can be rendered. The route structure is the following:

<Route path="/tab1">...</Route>
<Route path="/tab2">...</Route>
<Route path="/tab3">...</Route>
<Route path="/:tab/edit">...</Route>

With typesafe-routes, what would the structure be?

<Route path={tab1.template}>...</Route>
<Route path={tab2.template}>...</Route>
<Route path={tab3.template}>...</Route>
<Route path={"?"}>...</Route>

Options so far

  1. Route repetition.
<Route path={tab1.template}>...</Route>
<Route path={tab2.template}>...</Route>
<Route path={tab3.template}>...</Route>
<Route path={tab1({}).edit.template}>...</Route>
<Route path={tab2({}).edit.template}>...</Route>
<Route path={tab3({}).edit.template}>...</Route>

This allows us to have the edit route as a child of each tab route, but the repitition is of course a bit meh ๐Ÿ˜…

  1. react-router-like structure.
<Route path={tab1.template}>...</Route>
<Route path={tab2.template}>...</Route>
<Route path={tab3.template}>...</Route>
<Route path={edit.template}>...</Route>
// const edit = route('/:tab/edit', {tab: tabParser}, {})

This avoids the repetition, but defining the tabs as parameters with a custom parser is not nice.
This can also be changed to have the tabs always as params, but that doesn't make the parser situation nicer.

  1. know-the-current-tab hackshould be possible)
const tabRoute = [tab1, tab2, tab3].find((possibleTabRoute) =>  !relative(possibleTabRoute({}).$, location.pathname).startsWith('..'));

<Route path={tab1.template}>...</Route>
<Route path={tab2.template}>...</Route>
<Route path={tab3.template}>...</Route>
<Route path={tabRoute({}).edit.template}>...</Route>

This feels similar to 1, but it's nice that we can also use tabRoute to dynamically build an href to the edit route of the current tab: <Header editHref={tabRoute({}).edit({}).$} />

Solutions

This is where I'm not sure. Maybe some sort of path={combineRoutes(tab1, tab2, tab3).template} that generates a route matching /tab1 and /tab2 and /tab3?

Even stronger path typing

Let's consider this code.

const routes = createRoutes({
    groups: {
        path: ['groups', int('gid')],
        children: {
          users: {
            path: ['users', int('uid').optional],
            query: []
          },
        },
    },
});

routes.render("groups/users", { path: { gid: 123, uid:456 } });

The first argument of the render method is properly typed, i.e. this is a string union dynamically created from the nested properties of the routes. In this way typescript knows about all the possible path and can raise an error if it is wrong.

While string union of paths are much better than plain strings, TS (or webstorm) are unable to refactor those path when for instance we refactor a route property name.

Besides, it is also slightly confusing to write an url path like syntax to target the route to render (according to the feedback of a coworker of mine).

Here are some insets:
When I need to type path and when the path is related to an existing structure (interface) I avoid using template string typed path for the following reasons:

  • TS will generate all the possible path (string templates) and this slow down the compiler
  • TS Has a limit of 2^16 generated string template.
  • The string templates will lose the original context which prevents us from refactoring properly the properties

For those reasons, I prefer relying on https://www.npmjs.com/package/typed-path?activeTab=readme, I'm using this library to type the translation path in angular. It's not the sexyest syntax but it's much faster and more reliable than the strings.

Here is an example

import {typedPath} from 'typed-path';

type TestType = {
    a: {
        testFunc: () => {result: string};
        b: {
            arrayOfArrays: string[][];
            c: {
                d: number;
            };
        }[];
    };
};

console.log(typedPath<TestType>().a.b[5].c.d.$rawPath);

Behind the hood, the lib uses the interface to constrain the properties path but recursively creates proxies able to keep a ref on the parent to then print the path when we call the reserved property $rawPath (or $path).

The pro with this lib is that refactoring a property either from the path or from the object itself the change will be propagated everywhere. This is really helpful.

So, back to typesafe-routes. I suggest to find a way to achieve the same within the render argument.

Suggestions:

routes.render("groups/users", { path: { gid: 123, uid:456 } });

Could theoretically be rewritten to an array of keys

routes.render(["groups","users"], { path: { gid: 123, uid:456 } });

It should, but I do have a doubt that it will, work with refactoring 2 ways i.e. (from the key to the route and conversely).

Another possible option would be to provide a callback to get the key path from

routes.render((root)=>root.groups.users, { path: { gid: 123, uid:456 } });

While technically it is possible to achieve it in TS, I'm wondering how this would integrate with the current implementation.

Note that using the binding :

routes
    .bind('groups', { path: { gid: 0 } })
    .bind('users', { path: { uid: 0 } })
    .render();

Allows in some case to refactor 2 ways within webstorm (as there is no path but a single key) but we cannot from the route definion find back the usage of the properties within the routes (while it would for instance be the case with typed-path)

"Failed to parse source map" warnings

Seeing this in the console when starting a local dev server (webpack):

WARNING in ./node_modules/typesafe-routes/build/index.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/index.ts' file: Error: ENOENT: no such file or directory, open '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/index.ts'

WARNING in ./node_modules/typesafe-routes/build/parser.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/parser.ts' file: Error: ENOENT: no such file or directory, open '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/parser.ts'

WARNING in ./node_modules/typesafe-routes/build/react-router.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/react-router.tsx' file: Error: ENOENT: no such file or directory, open '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/react-router.tsx'

WARNING in ./node_modules/typesafe-routes/build/route.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/route.ts' file: Error: ENOENT: no such file or directory, open '/Users/eric/Code/frontend-react/node_modules/typesafe-routes/src/route.ts'

Angular - Optional parameters aren't counted as parameters

Issue

When using this library with Angular and making parameters optional
e.g.

	public static orders = {
		show: route('orders/:id?', { id: intParser }, {})
	};

and registering such route as so

const routes = [
	// Orders
	{
		path: AppRoutes.orders.show.template,
		component: OrdersPageComponent
	}
}

the route is indentified as so
orders/:id
where :id is a string rather than a parameter

Possible workaround

Using a preprocessing function that preprocesses the paths before giving them to Angular to register

const getRoute = (route: RouteNode<string, any, any>) => {
	//remove all instances of '?' from route
	if (route.template.includes('?')) {
		return route.template.replace(/\?/g, '');
	}

        return route.template;
}

and using it instead of the clear template

const routes = [
	// Orders
	{
		path: getRoute(AppRoutes.orders.show),
		component: OrdersPageComponent
	}
}

Possible solutions

1. Implementing the preprocessing function in the core

2. Moving the optional declaration onto the ParserMap e.g.

  public static orders = {
  	show: route('/orders/:id', { id?: intParser }, {})
  };

Proper way to compose routes

Hello,
in our project we do organise feature folders with all its dependencies so that we can share a feature folder between projects. Something like that:

import { createRoutes, int, RouteNodeMap } from 'typesafe-routes';

// feature/user.route.ts folder
export const Users = {
    list: {
        path: ['list'],
    },
    detail: {
        path: ['detail', int('uid')],
  },
} satisfies RouteNodeMap;

// feature/cart.route.ts folder
export const Cart = {
  detail: {
    path: ['detail'],
  },
} satisfies RouteNodeMap;


// app.route.ts folder
export const routes = createRoutes({
    user: {
        path: ['user'],
        children: Users,
    },
    cart: {
      path: ['cart'],
      children: Cart,
  },
});

I expected to be able to put a routeContext into another routeContext but it seems no to be possible (and there is no toRouteNodeMap converter (?)) so is there another way to compose routes or we are doing it the right way ?

Angular - Need for '/' for absolute routes in routeLink

Hi!
First of all thank you for helping with keeping my project typesafe.

Issue

However I've run into an issue recently
When routing from typescript this works just fine, because the relativity isn't based on the slash sign but rather on the { relative: true} option

	async onSubmit() {
                //...
		void this.router.navigateByUrl(AppRoutes.artists.show({ id: id }).$);
	}

However when routing from html using [routeLink] prop on a tag it causes routing to be relative due to lack of /

<li class="action">
	<a [routerLink]="AppRoutes.artists.show({ id: lastOccasion.id }).$">Continue</a>
</li>

Workaround

A possible workaround is to do something like this

<li class="action">
	<a [routerLink]="'/' + AppRoutes.artists.show({ id: lastOccasion.id }).$">Continue</a>
</li>

However that does not seem as too good of a thing to have to do

There is also a possibility to define routes like so:

	static artists = {
		show: route('artists/:id', { id: intParser }, {}),
		new: route('artists/new/:id', { id: intParser }, {}),
		update: route('artists/update/:id', { id: intParser }, {})
	};

However Angular routing system does not support registering routes that start with /
Returns an error

main.ts:7 Error: NG04014: Invalid configuration of route '/artists/:id': path cannot start with a slash
    at validateNode (router.mjs:2782:19)
    at validateConfig (router.mjs:2718:9)
    at Router.resetConfig (router.mjs:5086:60)
    at new Router (router.mjs:4970:14)
    at Object.Router_Factory [as factory] (router.mjs:5428:23)
    at R3Injector.hydrate (core.mjs:9290:35)
    at R3Injector.get (core.mjs:9178:33)
    at router.mjs:6340:33
    at core.mjs:27458:41
    at Array.forEach (<anonymous>)

Proposed solution

Addition of another property for getting the path alongside the $, for example .absolute or other syntax that basically does the same as $ but adds the slash at the beginning

React Router v6

I am currently using this library and have loved type-safe routing every since (and would hate going back to using plain react-router).

Is there a plan to update this library to be compatible with React Router v6?

I have been using this library so I have not used v6 and can't speak to the advantages of it directly over v5, BUT, from what I have seen describing v6, the emphasis on hooks (which, to be fair is already in v5), the <Routes> replacing <Switch>, portals, and generally the <Route> component and how it handles nested routes looks pretty promising.

`routeFn` assumes `this` is defined

Hi, first of all thanks a lot for creating this module. I'm introducing it in a medium-sized frontend project after noticing that it does pretty much exactly what I was starting to write, only with a slightly different API. So you may see at least a small usage spike soon ;)

I've hit two issues trying to adopt the library, one of which I'm reporting here, the other one I'll create a PR for.

Repro

Add the dependency, then node -p 'const {route} = require("typesafe-routes"); route("/root", {}, {})({}).$'

Expected

Prints /root

Actual

/tmp/asdf/node_modules/typesafe-routes/build/index.js:88
            var queryParams = __assign(__assign({}, _this.previousQueryParams), stringifyParams(parsedRoute.queryParamParsers, rawParams));
                                                          ^
TypeError: Cannot read property 'previousQueryParams' of undefined
    at Object.get (/tmp/asdf/node_modules/typesafe-routes/build/index.js:88:59)
    at [eval]:1:71    at Script.runInThisContext (vm.js:132:18)
    at Object.runInThisContext (vm.js:309:38)
    at internal/process/execution.js:77:19
    at [eval]-wrapper:6:22
    at evalScript (internal/process/execution.js:76:60)
    at internal/main/eval_string.js:23:3

Note

In the given repro, the this of routeFn will be undefined (which the function fails to deal with), unlike when doing node -p 'require("typesafe-routes").route("/root", {}, {})({}).$', which works as expected.

Has a dependency on react/react-router

index.ts re-exports react-router.ts, which depends on react and react-router-dom. So if a project doesn't have those dependencies, then it's impossible to use typesafe-routes.

I suggest react-router related dependencies are not re-exported from index.ts. So users that want to use react-router utilities can import it as typesafe-routes/react-router instead.

pageParams and queryParams dissociation

Hello, while using the following code:

export const pageParams = route(':id&:date', {id: stringParser, date: dateParser}, {});

I realize that parsing the route pageParams.parseParams({id:"", date:"2001-01-01"}) the route will merge the params with the queryParams, for this reason, I'm loosing the distinction typing when I want to retrieve only one of those.

in short: I expect to have the following typing

type pageParams = {
id: string
}

type pageQueryParams = {
date?: Date
}

but currently I do get a merged version of both

type params = {
id: string;
date?: Date
}

One possible dirty (?) way of getting back only the pageParams or the pageQueryParams would be to use a Pick<params, "id"> and Omit<params, "id"> to respectively hve the pageParams and the pageQueryParams. Of course the code above should rely on another type function able to identify within the string template the possible pageQueryParams keys.

Here is an idea:

type ExcludeAmpersandPrefix<S extends string> = S extends `&:${infer _}` ? never : S;

type StringToUnion<S extends string> = 
  S extends `:${infer T}:${infer U}` ? ExcludeAmpersandPrefix<T> | StringToUnion<`:${U}`> :
  S extends `:${infer T}` ? ExcludeAmpersandPrefix<T> :
  never;

Where

type Result = StringToUnion<":id:id2&:id3">; // "id" | "id2"

But of course this is taking the problem upside down (splitting after merging) while we could have the parseParams to be a merge of pageParams and pageQueryParams.

The same apply for serializing.

Right now, we also have a problem if the pattern is "':id&:id'", i.e. a clash between 2 property names while they could be distinct.

A possible workaround is to use 2 routes, one for the pageParams ":id" and the second one for the pageQueryParams "&:date" where it will work, it's clean (single responsibilit) but then I cannot have one object to provide the route to angular i.e. I cannot use the convenient pageParams.template.

Of course I can create a helper doing something like

function mergeTemplate(a: route, b: route){
[a.template, b.template].filter((template)=>!!template).join("/");
}

But I feel like we should have the ability to parse and serialize only the pageParams and queryPageParams if we want to from typesafe-routes.

Am I doing it wrong ?

[edit] I realize that angular won't ever need the template for the pageQueryParams

Home route "/" returns an empty string as url

Situation
The application defines a home route "/" and another route which is not nested.

const homeRoute = route("/", {}, {});
const aboutRoute = route("/about", {}, {});

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path={homeRoute.template} element={...} />
        <Route path={aboutRoute.template} element={...} />
      </Routes>
    </BrowserRouter>
  );
}

Expected
The home route should return "/" as the url. When used on a Link element it should result in href="/".

Actual
The home route returns an empty string as url which results in <Link to="" />. When the user is on the about page this will render a link that navigates to the current page <a href="/about">.

console.log(`Route: "${homeRoute({}).$}"`); // Route: ""

Repro: https://github.com/rothsandro/repro-typesafe-routes

Wrong routes in the documentation ?

Hi, regarding this file:

https://github.com/kruschid/typesafe-routes/edit/master/docs/basic-features/parameter-types.md

In many examples we do have

import { createRoutes, oneOf } from "typesafe-routes";

const options = oneOf("movies", "music", "art")

const routes = createRoutes({
  home: ["home", options("category")] // <- wrong
});

routes.render("home", {path: {category: "music"}}); // => "/home/music"
routes.parseParams("home", "/home/art"); // => {category: "art"}

i.e.
home: ["home", options("category")]

this won't compile, it should be

const routes = createRoutes({
    home: { path: ['home', options('category')] },
});

As I found this mistake in many places (in the same file) I was wondering if this is a real error or just a kind of upcomming shorthand for the path that I missed.

PeerDependency

Hello, after installing the lib (npm i typesafe-routes)
I got the following warning:

npm WARN Could not resolve dependency:
npm WARN peer typescript@"^4.1.0" from [email protected]
npm WARN node_modules/typesafe-routes
npm WARN typesafe-routes@"" from the root project
npm WARN
npm WARN Conflicting peer dependency: [email protected]
npm WARN node_modules/typescript
npm WARN peer typescript@"^4.1.0" from [email protected]
npm WARN node_modules/typesafe-routes
npm WARN typesafe-routes@"
" from the root project

This is likely because I'm using typescript 5.x.x and your dependence peerDependencies are:

https://github.com/kruschid/typesafe-routes/blob/master/package.json#L36

  "peerDependencies": {
    "typescript": "^4.1.0"
  },

you should replace with:

"peerDependencies": {
  "typescript": ">=4.1.0"
},

i.e. any typescript version above or equals to 4.1

How to use `useRouteParams` with nested routes?

For example:

import { route } from "typesafe-routes";

const detailsRoute = route("details&:id", { id: stringParser }, {})
const settingsRoute = route("settings", {}, { detailsRoute });
const accountRoute = route("/account", {}, { settingsRoute });

const MyComponent = () => {
  const params = useRouteParams(accountRoute)
  params.id // <- Error: `id` not exists
}

0 integer value in path params is ignored

If I create a route

const fooRoute = route("/root/:number", { number: intParser }, {});
fooRoute({ number: 0 }).$;

It's expected that the resulting path to be /root/0. But 0 is ignored so it only output /root

Testing against arbitrary URLs

Hello, this may be out of the scope of this library, but the core type-safe route declarations look great and I'd like to integrate it into my custom frontend framework. The one hurdle I'm currently stuck on is the ability to define a set of routes and test them against arbitrary (runtime) URLs.

The current parseParams API seems to be focused on parsing an already semi-parsed set of key/value pairs. This makes sense for compile-time routing but I'd like to extend the functionality to runtime routing using arbitrary URLs. I would imagine the API would look something like this:

const fooRoute = route("/foo/:id", {
     id: stringParser
}, {})

let res = fooRoute.parseUrl("/foo/bar")
// {"id": "bar"}

res = fooRoute.parseUrl("/hello/world")
// null or throw?

Is there existing functionality for this that I'm overlooking or is this something that seems like a reasonable extension to the API? I'm happy to investigate implementing it myself if you're open to PRs but the internals looks a little... dense at first glance.

Let me know your thoughts and thanks for the work on this excellent library!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.