Home > Software design >  Get Typescript type from mapped object
Get Typescript type from mapped object

Time:09-21

I'm trying to do the following:

shared.ts

export type ReverseMap<T> = T[keyof T];

/*
This is where i want to return a slightly modified type with the '/root/' appended to the values.
*/
export const getRoutesForPages = <T>( pages: T, root: string ): T => {
   Object.keys(pages).forEach((key) => {
      pages[key] = `/${root}/${pages[key]}`
   });
  return pages;
}

admin.ts

import { getRoutesForPages, ReverseMap } from './shared';

// root is basically the name of the lazy loaded module.
const ROOT = 'admin';

// pages are referenced by the routes within lazy loaded module
export const PAGES = {
  PAGE_1: `p-1`,
  PAGE_2: `p-2`,
} as const;
export type Page = ReverseMap<typeof PAGES>;
// gives me a type with 'p-1' and 'p-2'

/* creates a mapped routes object referenced throughout the application
export const ROUTES = {
  PAGE_1: `/admin/p-1`,
  PAGE_2: `/admin/p-2`,
} as const;
The issue is that it still returns interface of PAGES not the modified.
*/
const ROUTES = getRoutesForPages<typeof PAGES>(PAGES, ROOT);

// should create a type like '/admin/p-1' and 'admin/p-2'
export type Route = ReverseMap<typeof ROUTES>; // create a type from my route object

The type Route returns 'p-1' and 'p-2'.

I would like it to return '/admin/p-1' and '/admin/p-2' as the type.

Ideally done within the getRoutesForPages function for re-usability. My experience with generics and types is limited, so some help would be appreciated. Or if there is a better solution, please let me know.

CodePudding user response:

Add another generic parameter to your function to infer the type of the root parameter. Then in the return type you can use a mapped type and reference the root parameter with the generic parameter Root. With template literal types, it's practically doing the same thing as your code.

const getRoutesForPages = <T extends Record<any, string>, Root extends string>( pages: T, root: Root ): { [K in keyof T]: `/${Root}/${T[K]}` } => {
    pages = { ...pages };

    (Object.keys(pages) as (keyof T)[]).forEach((key) => {
        pages[key] = `/${root}/${pages[key]}` as T[keyof T]; // technically incorrect cast and only exists to suppress errors
    });

    return pages as ReturnType<typeof getRoutesForPages<T, Root>>;
};

Also, your original code mutates the pages parameter, meaning your PAGES constant actually got changed, which is a big no-no. That's why I have added pages = { ...pages }; to create a shallow copy we can modify.

You'll also notice the new type assertions in the body. We're doing icky things here so I think the easiest way is to just cast them to the right types.

const ROUTES = getRoutesForPages(PAGES, ROOT);

export type Route = ReverseMap<typeof ROUTES>;

You also don't have to explicitly pass any generics to your function as TypeScript can infer these for you. And your Route type is now "/admin/p-1" | "/admin/p-2" as desired.

Playground

  • Related