skip to Main Content

Assume I have this object routes which represent my website routes tree:

const r = {
  HOME: "start",
  ACCOUNT: {
    HOME: "account",
    PROFILE: "profile",
    ADDRESSES: {
      HOME: "addresses",
      DETAIL: ":addressId",
    },
  },
  ESHOP: {
    HOME: "eshop",
    INVOICES: "invoices",
    ORDERS: {
      HOME: "orders",
      DETAIL: ":orderId",
    },
  },
  INVENTORY: {
    HOME: "warehouse",
    CATEGORIES: {
      HOME: "categories",
      CATEGORY: {
        HOME: ":categoryId",
        PRODUCTS: {
          HOME: "products",
          PRODUCT: ":productId",
        },
      },
    },
  },
};

I’m trying to build a compact and elegant function to return the full path from a known object keys path, like this:

buildRoute(r, "r.ACCOUNT.ADDRESSES.DETAIL")
// should return this string: "start/account/addresses/:addressId"

or

buildRoute(r, "r.INVENTORY.CATEGORIES")
// should return this string: "start/warehouse/categories"

or

buildRoute(r, "r.INVENTORY.CATEGORIES.CATEGORY.PRODUCTS.PRODUCT")
// should return this string: "start/warehouse/categories/:categoryId/products/:productId"

Where "r" is my object routes, "HOME" is the fixed key of any path/subpath and the "slash" must be inserted in all subroutes except the last one.
Of course I’ll pass the data as a string, not a value.

How can i achieve this with clean and not too long code? Typescript solution including types would be better and appreciated.

2

Answers


  1. You could split the given string into its parts and then walk down the hierarchy accordingly, building the result string as you go:

    function buildRoute(node, path) {
        const keys = path.split(".");
        if (keys[0] != "r") throw "path must start with 'r.'";
        let result = "";
        for (const key of keys.slice(1)) {
            if (!Object.hasOwn(node, key)) throw `unknown key '${key}'`;
            result += node.HOME + "/";
            node = node[key];
        }
        return result + (typeof node === "string" ? node : node.HOME);
    }
    
    const r = {HOME: "start",ACCOUNT: {HOME: "account",PROFILE: "profile",ADDRESSES: {HOME: "addresses",DETAIL: ":addressId",},},ESHOP: {HOME: "eshop",INVOICES: "invoices",ORDERS: {HOME: "orders",DETAIL: ":orderId",},},INVENTORY: {HOME: "inventory",CATEGORIES: {HOME: "categories",CATEGORY: {HOME: ":categoryId",PRODUCTS: {HOME: "products",PRODUCT: ":productId",},},},},};
    
    console.log(buildRoute(r, "r.ACCOUNT.ADDRESSES.DETAIL"));
    // should return this string: "start/account/addresses/:addressId"
    console.log(buildRoute(r, "r.INVENTORY.CATEGORIES"));
    // should return this string: "start/warehouse/categories"
    console.log(buildRoute(r, "r.INVENTORY.CATEGORIES.CATEGORY.PRODUCTS.PRODUCT"));
    // should return this string: "start/warehouse/categories/:categoryId/products/:productId"
    Login or Signup to reply.
  2. If you want it compact, elegant, and to have autocompletions, use Proxy style, the same one libs like tRPC use

    type BuildProxy<T, S extends string> =
      T extends string ? {
        (): `${S}/${T}`
      } : T extends { HOME: infer H extends string } ? {
        [K in keyof T]: BuildProxy<T[K], `${S}/${H}`>
      } & {
        (): `${S}/${H}`
      } : never
    
    function buildProxy<T extends object>(o: T, path = ''): BuildProxy<T, ''> {
      return new Proxy(()=>{}, {
        // determines property getter
        get(f, p) {
          // if it's a string make it {HOME: string} instead
          let newO = typeof o[p] === 'string' ? { HOME: o[p] } : o[p]
          return buildProxy(newO, `${path}/${o.HOME}`)
        },
        // on call return the resulting path
        apply() {
          return `${path}/${o.HOME}`
        }
      }) as any;
    }
    
    let addr1 = buildProxy(r).ACCOUNT.ADDRESSES.DETAIL()
    //  ^?
    // let addr1: "/start/account/addresses/:addressId"
    console.log(addr1) // logs "/start/account/addresses/:addressId"
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search