Home > other >  How does React Router infer strongly typed arguments from path string?
How does React Router infer strongly typed arguments from path string?

Time:02-02

(This is a question for understanding the workings, not a "how do I..." question)

When working with React Router I've realized that Vscode IntelliSense can suggest me strongly-typed parameters when I type them in a route like this:

<Route path='/users/:userId?' render={props => <UserManager selectedId={props.}/>} />

When I type props.match.params., whenever I have in path string as :text goes as a strongly typed parameter from inside the string to the params as a strongly typed property (and an optional parameter if there is a question mark as in my case):

enter image description here

How can Vscode/TypeScript/React Router generate a strongly typed argument from a bare user-entered string?

CodePudding user response:

Please see this question just for reference.

You will find explanation in comments:

import React from 'react'

type PathParams<
  Path extends string
  > =
  /**
   * Check whether provided argument is valid route string with parameters 
   */
  (Path extends `:${infer Param}/${infer Rest}`
    /**
     * If yes, call PathParams recursively with rest part of the string 
     * and make it union with current param
     */
    ? Param | PathParams<Rest>
    /**
     * Otherwise, check if argument is standalone parameter, for instance ":userId"
     */
    : (Path extends `:${infer Param}`
      /**
       * If yes, return it
       */
      ? Param
      /**
       * Otherwise check if provided string is allowed route string
       * but without /, for instance "user:userId"
       */
      : (Path extends `${infer _Prefix}:${infer Rest}`
        /**
         * If yes, call recursively PathParams with this parameter
         */
        ? PathParams<`:${Rest}`>
        /**
         * If provided string is invalid - return never
         */
        : never)
    )
  )

/**
 * As you might have noticed, we ended up
 * with union of strings. Now we just need to convert this union
 * into record. It is the easiest part
 */
type Params = PathParams<":siteId/user/:userId">

/**
 * Convert union to record with appropriate keys
 */
type PathArgs<Path extends string> = { [K in PathParams<Path>]: string };

// type Result = {
//     siteId: string;
//     userId: string;
// }
type Result = PathArgs<"/dashboard/:siteId/user:userId">

Now lets try to build similar react component:


type Props<Path extends string> = {
  path: Path,
  params: PathArgs<Path>
}

const Route = <Path extends string>(props: Props<Path>) => {
  return null
}

const jsx = <Route path="/dashboard/:siteId/user:userId" params={{ siteId: 'hello', userId: 'world' }} /> // ok
const jsx_ = <Route path="/dashboard/:siteId/user:userId" params={{ siteId: 'hello' }} /> // error, missing userId prop

Playground

This example should give you a clue how it is done in react router. I know that my example does not reflect 100% same behavior, still I believe it is helpful

Please keep in mind, that my example is much smaller than actual implementation.

Original implementation you can find here with comments. Their implementation is much bigger because there might be a lot of edge cases.

  •  Tags:  
  • Related