DEV Community

Andrei Kondratev
Andrei Kondratev

Posted on

Useful types: Extract route params with TypeScript

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
Enter fullscreen mode Exit fullscreen mode

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 and template literal types. TypeScript also allows to use generics with all of these types. Using conditional types and generics you can write something like this

type ExctractParams<Path> = Path extends "card/:cardId" 
   ? { cardId: string }
   : {}

type Foo1 = ExctractParams<"card"> // {}
type Foo2 = ExctractParams<"card/:cardId"> // {cardId: string}
Enter fullscreen mode Exit fullscreen mode

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 to store an inferred type to a new type parameter and use the parameter as a result of the conditional type. Look at this

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}
Enter fullscreen mode Exit fullscreen mode

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

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}
Enter fullscreen mode Exit fullscreen mode

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 is

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"};

Enter fullscreen mode Exit fullscreen mode

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

Top comments (1)

Collapse
 
wzulfikar profile image
Wildan Zulfikar

Very cool! I have this routes file for a React Native app where I map the screen to the corresponding Next.js path (it's from Solito Starter). When I add new screen component, I need to update both the Routes and screens but I know TS can be smart enough to infer the Routes for me.

So I found your post and it's just what I need. Thanks!

Before:

// ./routes.ts – I need to update `Routes` and `screens` 
// when adding new screen.

// Type of params for each screen
export type Routes = {
  home: undefined
  chats: undefined
  login: undefined
  'user-detail': { id: string }
  logout: { logout?: boolean }
}

// Map screens to paths
const screens: Record<keyof Routes, string> = {
  home: '',
  'user-detail': 'user/:id',
  chats: '/chats',
  login: '/login',
  logout: '/logout',
}

export const routes = {
  initialRouteName: 'home',
  screens,
} as const
Enter fullscreen mode Exit fullscreen mode

After:

// ./routes.ts – now I only need to update `screens`
// because `Routes` is inferred.

// Map screens to paths
const screens = {
  home: '',
  'user-detail': '/user/:cardId',
  chats: '/chats',
  login: '/login',
  logout: '/logout',
} as const

export const routes = {
  initialRouteName: 'home',
  screens,
} as const

// ./types.ts – this file is just here to define types so 
// I can `import { Routes } from "./types.ts"` from 
// somewhere else (see usage).

import { routes } from './routes'

const screens = routes.screens

type ExtractParam<
  Path extends string,
  NextPart
> = Path extends `:${infer Param}` ? Record<Param, string> & NextPart : NextPart

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

// Type of params for each screen
export type Routes = {
  [Screen in keyof typeof screens]: ExtractParams<typeof screens[Screen]>
}
Enter fullscreen mode Exit fullscreen mode

Usage of Routes for @react-navigation/native-stack:

import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { Routes } from "./types.ts"

const Stack = createNativeStackNavigator<Routes>()
Enter fullscreen mode Exit fullscreen mode