Search code examples
typescripttypescript-generics

How do i write recursive Routes type with absolute paths?


I have react-router v6 in my app, and i want all paths in router config object to be absolute

But i need to ensure that path of child route of any depth begins with its parent path, otherwise app just might crash

Seems like i need a recursive type with template literals

Type i've come with (just path and children to simplify):

type Route<T extends string> = {
  path: T;
  children?: Route<`${T}/${string}`>[];
};

But it doesn't work for nested routes, T generic becomes of string type

const invalidRoute: Route<"/home"> = {
  path: "/home",
  children: [
    {
      path: "/home/about",
      children: [
        {
          // type `/home/${string}/${string}` here, type `/home/about/${string}` expected
          // so no typescript error here is raised
          path: "/home/nonabout/whatever", 
        },
      ],
    },
  ],
}

Could you help?


Solution

  • Unfortunately, you can't do this in typescript. Your Route type itself is saying the child can be anything, so of course when you call the type recursively, T becomes /home/${string}. Because this really matters on instantiation, I would suggest having a function which validates your Route and throws if it isn't valid.

    type Route = {
      path: string;
      children?: Route[];
    };
    
    const route: Route = {
      path: "/home",
      children: [
        {
          path: "/home/about",
          children: [
            {
              path: "/home/about/whatever1",
            },
            {
              path: "/home/about/whatever2",
            },
          ],
        },
        {
          path: "/home/something",
          children: [
            {
              path: "/home/notSomething/whatever1",
            },
            {
              path: "/home/something/whatever2",
            },
          ],
        },
      ],
    } as const;
    
    /**
     * Validates the react router route object contains paths which are absolute
     * @throws TypeError when child path is not prefixed with parent path
     */
    function enforceAbsoluteRoutes(route: Route): void {
      const parentPath = route.path;
      if (route.children) {
        for (const child of route.children) {
          if (!child.path.startsWith(parentPath)) {
            throw new TypeError(
              `Invalid path (${child.path}) is not prefixed with parent path (${parentPath})`
            );
          }
          enforceAbsoluteRoutes(child);
        }
      }
    }
    
    enforceAbsoluteRoutes(route);
    

    I had a similar problem where I wanted to convert an object to the string dot notation. I had to define a depth, start at the farthest child and work up the tree to create my string. However, this would only work if all your paths had the same children depth. This is incredibly complex and would not work for your situation.

    Typescript Playground