typescriptdenooak

How to dynamically create routes?


I'm trying to dynamically add routes to my api by iterating over an array of objects called route groups. A route group can have many routes.

Here are my types (the RouterContext type comes from Oak middleware framework):

// routes/mod.ts
type Route = {
  method: string;
  path: string;
  handler: (ctx: RouterContext) => Promise<void>;
};

export type RouteGroup = {
  group: {
    prefix: string;
  };
  routes: Route[];
};

Here is my router class:

export class Router {
  // imported Oak's Router class as oakRouter
  router: oakRouter;

  constructor() {
    this.router = new oakRouter({ prefix: "/api" });
  }

  register(): void {
    this._createRoutes(routeGroups);
  }

  private _createRoutes(routeGroups: RouteGroup[]) {
    routeGroups.forEach(({ group, routes }) => {
      routes.forEach(({ method, path, handler }) => {
        this.router[method](group.prefix + path, handler); // <-- this.router[method] is underlined with an error
      });
    });
  }
}

A typical route looks like this:

router.get("/", (ctx) => {
    ctx.response.body = "Welcome to My Oak App.";
});

But when I use bracket notation to dynamically add the http method that I want to use in _createRoutes(), I get the following error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Router<RouteParams, Record<string, any>>'.
  No index signature with a parameter of type 'string' was found on type 'Router<RouteParams, Record<string, any>>'.deno-ts(7053)

How do I change the method property on the Route type from a string into a valid index signature? Is that even what I need to do?


Solution

  • The simplest and least type-safe approach would be to cast your router function to any if you're confident you know better than the TypeScript compiler:

    (this.router[method] as any)(group.prefix + path, handler);
    

    or

    (<any>this.router[method])(group.prefix + path, handler);
    

    This bypasses a lot of type-checking which usually isn't what we want.


    Another approach is to use a type guard for each method supported by Oak Router (e.g. via a switch statement) which is type-safe but does duplicate some code:

    const args = [group.prefix + path, handler] as const;
    switch (method) {
      case "all":
        this.router[method](...args);
        break;
      case "delete":
        this.router[method](...args);
        break;
      case "get":
        this.router[method](...args);
        break;
      case "head":
        this.router[method](...args);
        break;
      case "options":
        this.router[method](...args);
        break;
      case "patch":
        this.router[method](...args);
        break;
      case "post":
        this.router[method](...args);
        break;
      case "put":
        this.router[method](...args);
        break;
    }
    

    A third option is to enumerate the supported method function names in Route.method and then tell the TypeScript compiler to treat all of the functions as the same function type (picking any of the function names, e.g. get):

    type Route = {
      method:
        | "all"
        | "delete"
        | "get"
        | "head"
        | "options"
        | "patch"
        | "post"
        | "put";
      path: string;
      handler: (ctx: RouterContext) => Promise<void>;
    };
    

    Then inside of _createRoutes:

    (this.router[method] as oakRouter["get"])(group.prefix + path, handler);
    

    This is more type-safe than the first approach but less type-safe than the second approach.


    P.S. I suggest renaming oakRouter to OakRouter. Class and type names typically start with an upper case letter in JavaScript/TypeScript.