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 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}
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}
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}
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"};
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 (2)
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
andscreens
but I know TS can be smart enough to infer theRoutes
for me.So I found your post and it's just what I need. Thanks!
Before:
After:
Usage of
Routes
for @react-navigation/native-stack:Thank you for your type function.
Other than that you have a typo in ExctractParams. It should be: Extract instead of Exctract