1

I have declared a type for extracting params from url:

type ExtractParams<Path extends string> =
  Path extends `${infer Start}/:${infer Param}/${infer Rest}`
    ? ExtractParams<Start> & ExtractParams<Rest> & { [Key in Param]: string; }
    : Path extends `${infer Start}/:${infer Param}`
      ? ExtractParams<Start> & { [Key in Param]: string }
      : {}

code works fine:

type X1 = ExtractParams<'/courses/:courseId/classes/:classId'>
//   ^? { classId: string; } & { courseId: string; }

Here is the Playground Link

But there are 2 things that can be improved here:

  1. Avoid repetition

If you check ExtractParams type definition, you can see that I have used nested conditions. Outer condition finds Params between /: and /. Inner condition finds params at the end of the string. So, I was thinking of declaring /${infer Rest} as optional somehow. But I don't know how can I declare something as optional in template literal type. Can you please help me to do and explain that?

2. Merge Intersection

The output looks ugly at the moment because each param gets its own object. Is there any way to merge those objects?

Update:

Got an answer for Merging Intersections from this answer: https://stackoverflow.com/a/58965725/2284240

To Merge intersections in my code, I can update it like this:

type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

type Params<Path extends string> = Expand<ExtractParams<Path>>;

type X1 = Params<'/courses/:courseId/classes/:classId'>
//   ^? { classId: string; courseId: string }

Updated Playground Link

Not posting it as an answer because, I am still in search of a solution to Avoid repetition

Vishal
  • 6,238
  • 10
  • 82
  • 158
  • Does [this approach](https://tsplay.dev/ND8gzN) meet your needs? If so I'll write up an answer explaining; if not, what am I missing? – jcalz Apr 09 '23 at 14:37
  • @jcalz Thanks for helping me over and over again. The approach you suggested is really a nice and easy to understand. Actually, I would have loved to use that approach, but before you posted the approach, I left for dinner with family. At that time, I already updated my code, but forgot to update the question with appropriate code. Now, I have added Update 2 section in question itself. Can you please check it? – Vishal Apr 09 '23 at 20:04
  • Why are you expanding the scope of the question instead of asking a separate one? – jcalz Apr 09 '23 at 20:09
  • Sorry for that one, but if I ask another question, then this question will be useless for me. – Vishal Apr 09 '23 at 20:14
  • Right, but you're not the only person on Stack Overflow. The question as asked would indeed be useful for future readers, and you could ask a new question that clearly lays out the new requirements as a single question and not a series of updates. I would be happy to look at a new question, but on principle I can't entertain questions that expand in scope to such an extent. Do you want to revert back to the old version? Or should I just disengage? – jcalz Apr 09 '23 at 20:23
  • @jcalz Please post your approach as answer, I will post a new question and will delete 2nd Update from the question soon. – Vishal Apr 09 '23 at 20:26
  • I have removed the additional information given in Update 2 in question. – Vishal Apr 09 '23 at 20:27
  • Once you post the new question it's possible [this approach](https://tsplay.dev/N9OQjW) might be what you want, but it depends strongly on the rules around where the parentheses can and cannot appear in your paths. We can discuss later, but try to make sure your new question explicitly mentions what those rules are (e.g., `a/(:b)` vs `a(/:b)` vs `(a/):b` vs `a(/:b)/:c` vs `(/a/:b)/c/:d(/e/:f)` etc etc etc) – jcalz Apr 09 '23 at 20:40
  • Sure, just doing that. Thanks for the answer.\ – Vishal Apr 09 '23 at 20:42
  • @jcalz I have posted another question here: https://stackoverflow.com/questions/75972814/avoid-repetition-in-template-literal-type-when-having-optional-routes – Vishal Apr 09 '23 at 20:51

1 Answers1

1

My recommended approach would be to split your path by "/" characters into a union of path segments:

type PathSegments<T extends string, A extends string = never> =
  T extends `${infer L}/${infer R}` ? PathSegments<R, A | L> : A | T;

which is a tail recursive conditional type and produces something like this for your example:

type PS1 = PathSegments<'/courses/:courseId/classes/:classId'>
// type PS1 = "" | "courses" | ":courseId" | "classes" | ":classId"

Then you can use just those segments which begin with a : character as the keys for your output type. We can do it this way

type Params<T extends string> = {
  [K in PathSegments<T> as K extends `:${infer P}` ? P : never]:
  string };

using key remapping to suppress any union members that don't begin with :. For your example that produces

type X1 = Params<'/courses/:courseId/classes/:classId'>
/* type X1 = {
    courseId: string;
    classId: string;
} */

which looks like what you want.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360