Useful types: Extract route params with TypeScript
Tags: typescript, react, tutorial
Published: 2022-05-02 21:57:09

Recently, I started working on a new project with React and TypeScript. I used `react-router-dom@6` for routing and a few of my routes look like this
```
/card
/card/:cardId
/card/:cardId/transaction
/card/:cardId/transaction/:transactionId
// ... and so on
```
So, I really love TypeScript and always try to create strong typed system especially if types will be inferred automatically. I tried to define a type that would infer a route's parameters type using the path. This is a story about how to do it.

As you know TypeScript has [conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) and [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). TypeScript also allows to use [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) with all of these types. Using conditional types and generics you can write something like [this](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogHgY2AJwIZIAqrQWwM4A8WwAFlBHMBAHYAmeUeKAltQOYB8UAvFMWRSp0GAIgTZaAegBc45LQCStEVABQUDQH4oAbyhzFtaYxbsoAX3UbjOy6tCQoAMQD2LgIw9YiFOmBZcQjEJES5JSV1LB2hXFwAmL3gkNExsVHwCYPkZAyVQqHDdXKMTZFY2cyA)
```typescript
type ExctractParams<Path> = Path extends "card/:cardId" 
   ? { cardId: string }
   : {}

type Foo1 = ExctractParams<"card"> // {}
type Foo2 = ExctractParams<"card/:cardId"> // {cardId: string}
```
Inside a conditional type we can use a template literal type to discover a parameter in the path. Then we can use an [`infer` keyword](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types) to store an inferred type to a new type parameter and use the parameter as a result of the conditional type. Look at [this](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogHgY2AJwIZIAqrQWwM4A8WwAFlBHMBAHYAmeUeKAltQOYB8UAvFMWRSp0GAAwBcAEgDerAGYRkfbKhwBfEQCgo2gPxQAShAQB7ZLSLKcAGkYt2HLdrFQpqjRtCQoAIWwBGHlhEFHRgLFxCACIEbFpIrgB6BJc3T2hfZAAmQPgkNExLKLEYswBJOMTkqRLacucmZFY2VPB07ABmHOD8sMKCSLEQ6jxQ5mNqcvioJJchkaQxidp6u2agA)
```typescript
type ExctractParams<Path> = Path extends `:${infer Param}`
   ? Record<Param, string>
   : {}

type Bar1 = ExctractParams<"card"> // {}
type Bar2 = ExctractParams<":cardId"> // {cardId: string}
type Bar3 = ExctractParams<":transactionId"> // {transactionId: string}
```
It's OK, but what about more complex path? We can also use the `infer` and template types to split the path into segments. The main idea of the extracting type is to split off one segment, try to extract a parameter from this segment and reuse the type with the rest of the path recursively.
It may be implemented like [this](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogHgY2AJwIZIAqrQWwM4A8WwAFlBHMBAHYAmeUeKAltQOYB8UAvFMWRSp0GAAwAkAb1YAzCMigBlCGxw1gAXwD0kmXKgAlCE3UiAUFCgB+RctXVg5SjXpQRALh3VZ8rLhNWDCAQAe2RaImxUHAAaRhZ2LgAyWEQUdGBfKMJDJi43FKQ0TEj8AhzgDnMofP5HIRd3T28+Ev9rQxCwiNxYpmRWTmqoCXVTU1BIKAAhVAAvAEYeArTi3EIAIgRsWnWuTU1h0YnoGdmAJiX4QvTM0s3tzTctsIBJHb2DiWfaN-y+gaO4BOcwAzJdUkUMiUNt9Ht83po0tQ8OlmMFqLsoPthvDaH94mxAZNTgAWcHXVZZAj3MJw7YIpEopBo6iPRmo9FvTHYr70vFxfrsWLs5mc-n-djqIA)
```typescript
type ExctractParams<Path> = Path extends `${infer Segment}/${infer Rest}`
  ? Segment extends `:${infer Param}` ? Record<Param, string> & ExctractParams<Rest> : ExctractParams<Rest>
  : Path extends `:${infer Param}` ? Record<Param, string> : {}

type Baz1 = ExctractParams<"card"> // {}
type Baz2 = ExctractParams<"card/:cardId"> // {cardId: string}
type Baz3 = ExctractParams<"card/:cardId/transaction"> // {cardId: string}
type Baz4 = ExctractParams<"card/:cardId/transaction/:transactionId"> // {cardId: string, transactionId: string}
```
But in this case, if the path can't be splitted by segments we have to try to extract the parameter from `Path` because it may be the last segment of a complex path that can contain some parameter. So, we have a duplicated path extracting logic. I suggest separating this logic into another type. My [finally solution](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogHsATgQwMbAArJQWwDxbAAWUECEAdgCYDOUNSAlhQOYA0UAcmZtsAHxQAvFEIkelWlAAGALgAkAb2YAzCIlHZkOAL7SoAfigAlCKgD2iKgS04ODRMxaCAZFx5ZEwKLPcJPwADcAFDBoJCwcOgo6J7aNDbEpOTUdA5OgiJiycCSdNJKqupQAMoQLDiUwDoA9IUUahqmDHrBUIaRSGi8uHhlFVUc8NHdcTgJzQL8bT6dMT3aiUQcijrTwRYUDFAq5ua+w12xtgkARKjYVKeZUKshm9sARtgHUUcL43jnlzWyF1YASSuN0U-yoQN8pxo5kqUEYVx093MW28zwAXq8RsdcGcwb8wUCal0tt1GMjfsSaKTkUDrsJbgSqJDobD4acOJTqRQIVBTsgKOZiMU2TpQsEajUAALAGgAWjIkHQ8sQiEsG2R2wA7qrWAAhF6RLEfXE-P6XWkgxHiqUy+VwRXAZWqxDqlFQbXIlj6jGG95jE1WfHmqhElAk9BkigUsNUiM04H00HB5kw6AikJAA) is 
```typescript
type ExtractParam<Path, NextPart> = Path extends `:${infer Param}` ? Record<Param, string> & NextPart : NextPart;

type ExctractParams<Path> = Path extends `${infer Segment}/${infer Rest}`
  ? ExtractParam<Segment, ExctractParams<Rest>>
  : ExtractParam<Path, {}>

const foo: ExctractParams<"card"> = {};
const bar: ExctractParams<"card/:cardId"> = {cardId: "some id"};
const baz: ExctractParams<"card/:cardId/transaction/:transactionId"> = {cardId: "some id", transactionId: "another id"}


//@ts-expect-error
const wrongBar: ExctractParams<"card/:cardId"> = {};
//@ts-expect-error
const wrongBaz: ExctractParams<"card/:cardId/transaction/:transactionId"> = {cardId: "some id"};

```
I use a special type parameter `NextPart` to determine what it has to do after extracting - try to extract parameters from the rest of the path or stop recursion.

I hope this story's been useful for you, you've learned something new. Maybe now you can improve something in your project.

Later I'm going to write a story about how I've implemented a route tree with the extracting parameters type and how I've used that with React and `react-router-dom`. Thanks