Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

typed-route-config

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

typed-route-config

Utility to created route configuration that is typed

  • 2.0.5
  • latest
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
2
decreased by-71.43%
Maintainers
1
Weekly downloads
 
Created
Source

Typed Route Config

Type safe configuration for routes.

Installation

npm install typed-route-config

The Problem

If you use react-router, you might want to centralise your routes in some kind of configuration object, which can then be looked up in all the places you use them.

For example, you may have:

import React from 'react'
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'

export const App: React.FC = () => (
    <BrowserRouter>
        <Switch>
            <Route path={'/dashboard/me'} render={() => <div>the dashboard profile component here...</div>}/>
            <Route path={'/dashboard/offer/:offerId/documents'} render={() => <div>the documents component here...</div>}/>
        </Switch>
    </BrowserRouter>
)

type LinkProps = {
    offerId: string
}

export const LinkToDocuments: React.FC<LinkProps> = ({ offerId }) => (
    <Link to={`/dashboard/offer/${offerId}/documents`} />
)

So, suppose you to factor these routes out into a configuration that looked like this:

// myRoutes.ts
export const mySimpleRouteConfig = {
    'dashboard':                        '/dashboard',
    'dashboard.me':                     '/dashboard/me',

    'dashboard.offer':                  '/dashboard/offer/:offerId',
    'dashboard.offer.collaborators':    '/dashboard/offer/:offerId/collaborators',
    'dashboard.offer.documents':        '/dashboard/offer/:offerId/documents',
}

Which would then look something like this:

import React from 'react'
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'
import { mySimpleRouteConfig } from './myRoutes'

export const App: React.FC = () => (
    <BrowserRouter>
        <Switch>
            <Route path={mySimpleRouteConfig['dashboard.me']} render={() => <div>the dashboard profile component here...</div>}/>
            <Route path={mySimpleRouteConfig['dashboard.offer.documents']} render={() => <div>the documents component here...</div>}/>
        </Switch>
    </BrowserRouter>
)

type LinkProps = {
    offerId: string
}

export const LinkToDocuments: React.FC<LinkProps> = ({ offerId }) => {
    const documentsPath = mySimpleRouteConfig['dashboard.offer.documents'] // '/dashboard/offer/:offerId/documents'
    
    // interpolate the offerId into documentsPath...
    const documentsUrlForThisOfferId = interpolate(documentsPath, { offerId })
    return (
        <Link to={documentsUrlForThisOfferId} />
    )
}

The issue here is you have no typesafety with how you've implented the interpolate function!

The Solution

Using typed-route-config could re-write your config as:

// myRoutes.ts
import { createRoutes } from 'typed-route-config';

export const { route, path } = createRoutes(root => ({
    'dashboard':                        root.path('dashboard'),
    'dashboard.me':                     root.path('dashboard/me'),

    'dashboard.offer':                  root.path('dashboard/offer').param('offerId'),
    'dashboard.offer.collaborators':    root.path('dashboard/offer').param('offerId').path('collaborators'),
    'dashboard.offer.documents':        root.path('dashboard/offer').param('offerId').path('documents')
}))

And then continuing with our example, we would then get

import React from 'react'
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'
import { route, path } from './myRoutes'

export const App: React.FC = () => {

    // const fakePath = path('fake.route')
    // Argument of type '"fake.route"' is not assignable to parameter of type '"dashboard" | "dashboard.me" | ... | "dashboard.offer.documents"

    // path() will only let you use keys from the config! It returns the un-interpolated version of that route
    // e.g. path('dashboard.offer.documents') -> 'dashboard/offer/:offerId/documents'
    return (
        <BrowserRouter>
            <Switch>
                <Route path={path('dashboard.me')} render={() => <div>the dashboard profile component here...</div>}/>
                <Route path={path('dashboard.offer.documents')} render={() => <div>the documents component here...</div>}/>
            </Switch>
        </BrowserRouter>
    )
}

type LinkProps = {
    offerId: string
}

export const LinkToDocuments: React.FC<LinkProps> = ({ offerId }) => {
    // route() interpolates the offerId into path for that route, but most importantly in a typesafe way
    const documentsUrlForThisOfferId = route('dashboard.offer.documents', { offerId })

    // const error = route('dashboard.offer.documents', { monkeyId: 'abc' })
    // Argument of type '{ monkeyId: string; }' is not assignable to parameter of type '{ offerId: string; }'

    return (
        <Link to={documentsUrlForThisOfferId} />
    )
}

We can go a step further and neaten up our route config to remove the repetition. You can use the .group(...) to nest routes. The config from before could be re-written as:

import { createRoutes } from 'typed-route-config';

export const { routes, route, path } = createRoutes(root => ({
    'dashboard': root.path('dashboard').group(dashboard => ({
        // '' within a group gives us a "root" route with the name of the group i.e. 'dashboard'
        '': dashboard,

        // joins the group name to this route name i.e. 'dashboard.me'
        'me': dashboard.path('me'),

        // and you can nest groups
        'offer': dashboard.path('offer').param('offerId').group(offer => ({
            '':                 offer,                       // 'dashboard.offer'
            'collaborators':    offer.path('collaborators'), // 'dashboard.offer.collaborators'
            'documents':        offer.path('documents')      // 'dashboard.offer.documents'
        }))
    })),
}))

Usage

Define your routes somewhere in your application using the createRoutes function:

// routes.ts
import { createRoutes, MakeRouteParams } from 'typed-route-config'

const { routes, route, path } = createRoutes(root => ({
    // ... routes go here
}))

// only export the functions you need, there are more explained later not shown in this basic example
// you probably don't want to export 'routes' as it is just used to generate the helper types below
export { route, path }

type Routes = typeof routes

// union type of every routeName
export type RouteNames = keyof Routes

// gets the route params object for the given routeName
export type RouteParams<N extends RouteNames> = MakeRouteParams<Routes, N>

TODO explain the rest of the API

Building from source

To build, just run npm run build

To make a change

Make your change, (don't forget to npm run build), then increment the version number in the package.json before pushing.

In the projects which depend on this package, run npm update typed-route-config.

npm detects changes using the version number in the package.json

FAQs

Package last updated on 23 Jan 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc