Home > Back-end >  Generate a list of types / interfaces from a mapping
Generate a list of types / interfaces from a mapping

Time:09-22

Given the following object:

const ROUTES = {
  PAGE_NO_PARAMS: '/hello/page/two',
  PAGE_R: '/about/:id',
  PAGE_Z: '/page/page/:param/:id',
  PAGE_N: '/who/:x/:y/:z/page',
} as const

Is it possible a list of types / interfaces for each route so that the developer is restricted to the valid params for the selected route.

Ie. generate a type from ROUTES that has the same outcome as type RouteAndParams below.

interface PageNoParams = {
   route: '/hello/page/two'  // no params
}

interface Param1 = {
   route: '/about/:id',
   params: { id: string } // required params
}

interface PAGE_Z = {
   route: '/page/page/:param/:id',
   params: { id: string; param: string } // required params
}

interface Param3 = {
   route: '/who/:x/:y/:z/page',
   params: { x: string; y: string; z: string } // required params
}

type RouteAndParams = PageNoParams | Param1 | PAGE_Z | Param3;

// Some examples of expected results / errors when using generated type

// should NOT error
const routeWithParams: RouteAndParams = {
   route: '/page/page/:param/:id',
   params: { 'param': 'blah', 'id': 'xxx' }
}

// should error as unexpected param 'x'
const routeWithParams: RouteAndParams = {
   route: '/about/:id',
   params: { 'id': 'xxx', 'x': 'xxx' }
}

// should error as param 'y' is missing
const routeWithParams: RouteAndParams = {
   route: '/who/:x/:y/:z/page',
   params: { 'x': 'blah', 'z': 'blah' }
}

I hope this all makes sense. I'm trying to replace runtime errors with build errors.

CodePudding user response:

You can use some recursive conditional types to process the path and extract the parameter from each path:


type Routes = MakeValidRoute<typeof ROUTES[keyof typeof ROUTES]>
 // = "/hello/page/two" | `/about/${string}` | `/page/page/${string}/${string}` | `/who/${string}/${string}/${string}/page`
 
type GetInterfaceKeys<T extends string, R extends string = never> =
  T extends `${string}/:${infer Name}/${infer Tail}`?
    GetInterfaceKeys<`/${Tail}`, R | Name>:
  T extends `${string}/:${infer Name}`?
    R | Name:
    R

type MakeRouteAndParams<T extends Record<string, string>>  = {
  [P in keyof T]: {
    route: T[P],
    params: Record<GetInterfaceKeys<T[P]>, string>
  }
}[keyof T]

type RouteAndParams = MakeRouteAndParams<typeof ROUTES>
// type RouteAndParams = {
//     route: "/hello/page/two";
//     params: Record<never, string>;
// } | {
//     route: "/about/:id";
//     params: Record<"id", string>;
// } | {
//     route: "/page/page/:param/:id";
//     params: Record<"id" | "param", string>;
// } | {
//     ...;
// }

Playground Link

We use tail recursive conditional types to improve performance of these types in the compiler

CodePudding user response:

Here's one approach:

type ParamNames<T extends string, A extends string = never> =
  T extends `${infer F}:${infer E}/${infer R}` ?
  ParamNames<R, A | E> : T extends `${infer F}:${infer E}` ?
  A | E : A    

ParamNames<T> is a tail-recursive conditional type that turns a string including colon-delimited path parameters into a union of those parameter names. For example:

type Test = ParamNames<"/ay/:bee/cee/:dee">
// type Test = "bee" | "dee"

Then you can turn this into your route-and-params pairs with another conditional type:

type RouteAndParams<T extends string> = T extends unknown ?
  ParamNames<T> extends infer S extends string ?
  [S] extends [never] ?
  { route: T } :
  { route: T, params: { [K in S]: string } }
  : never : never

It's a little fiddly because we suppress the params property if ParamNames<T> turns out to be never for a particular member. Let's test it:

const ROUTES = {
  PAGE_NO_PARAMS: '/hello/page/two',
  PAGE_R: '/about/:id',
  PAGE_Z: '/page/page/:param/:id',
  PAGE_N: '/who/:x/:y/:z/page',
} as const

type RP = RouteAndParams<typeof ROUTES[keyof typeof ROUTES]>
/* type RP = {
    route: "/hello/page/two";
} | {
    route: "/about/:id";
    params: {
        id: string;
    };
} | {
    route: "/page/page/:param/:id";
    params: {
        id: string;
        param: string;
    };
} | {
    route: "/who/:x/:y/:z/page";
    params: {
        x: string;
        y: string;
        z: string;
    };
} */

Looks good.

Playground link to code

  • Related