Disclaimer: This story is focused on the storytelling aspect of my experience. For the sake of flow and engagement, I’ve skipped detailed code explanations and some technical nuances. The goal here is to share the journey, not to provide a deep technical breakdown.
Introduction: A Lesson in Humility and TypeScript Wizardry
Every developer has that moment—the one where they think they've mastered something, only to be completely humbled by a colleague. This is the story of my moment. It started with a simple goal: improving routing in a React project. It ended with me questioning my entire existence as a developer. 😅
What follows is a journey filled with excitement, frustration, moments of pure dopamine-fueled breakthroughs, and ultimately, an eye-opening realization about experience, skill, and what it really means to be a senior dev. Buckle up. 🚀
The Setup 🎬
It was another day of development, another day of feeling like a pro. I was working on a React project, using createBrowserRouter
to handle the routing in Router.tsx
. Notice the .tsx extension? Yeah, that’s because createBrowserRouter
requires actual JSX components inside element
. Let me show you an example of what the router looked like:
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout/>,
children: [
{
path: "",
element: <ProtectedRoute><DashboardLayout/></ProtectedRoute>,
children: [
{
index: true,
path: "projects",
element: <OnlyAdmin fallback="/all-projects"><Projects/></OnlyAdmin>,
},
{
path: "projects/:projectId",
element: <OnlyAdmin><ProjectGuard/></OnlyAdmin>,
children: [
{
path: "groups",
element: <ProjectGroups/>
},
{
id: "project-stats",
path: "groups/:groupId",
element: <GroupGuard/>,
children: [
{
path: "stats",
element: <GroupStats/>
},
{
path: "consents",
element: <Consents/>
}
]
},
{
path: "stats",
element: <ProjectStats/>
}
]
},
fallbackRoute("/projects")
]
}
]
},
{
path: "*",
element: <NoMatch/>,
}
], {
future: {
v7_startTransition: true,
}
});
I loved using createBrowserRouter
. It was clean, it allowed nested routes, and I highly recommend it if you’re working with React Router. But then the problem hit.
The Problem 🤯
Across the project, we were using the <Link>
component to navigate. Pretty standard, right? Something like:
<Link to="/projects/123/groups/456">Go to Group</Link>
Now, here’s where the chaos began. The client requested a change in the routing structure. Just some small tweaks to paths, nothing major… right? WRONG.
Because now, every single manually written URL in the project had to be updated. 🥲
I was staring at the codebase, thinking: Okay, this is bad. This is really bad.
There had to be a better way—a way to validate links across the app without relying on hardcoded strings, a way to autocomplete routes when writing them, a way to ensure consistency.
My brainstorming mode activated.
🔍 How could I make the routes smarter?
💡 Could TypeScript help enforce route validation?
🤔 How do I make WebStorm autocomplete route names for me?
Then, the stupidest but funniest idea hit me: Should I just make a JetBrains plugin for this? 😂
For a solid five minutes, I was seriously considering writing an entire JetBrains plugin just to solve my routing issue. But then, I shook my head. Hichem, stop. The company is not paying you to build useless plugins.
The "Brilliant" Plan 🚀
Instead, I thought: let’s use TypeScript.
If I could somehow convert route paths into TypeScript-enforced IDs, I could:
✔ Validate links.
✔ Enable autocomplete.
✔ Ensure routes are always correct, no more broken links.
I modified the router to include unique IDs for each route:
const routeObjects: RouteObject[] = [
{
path: "/",
element: <AppLayout/>,
id: "root",
children: [
{
path: "projects",
element: <OnlyAdmin><Projects/></OnlyAdmin>,
id: "admin-projects-list"
}
]
}
];
I was feeling like a genius. But TypeScript wasn’t cooperating.
I needed TypeScript to dynamically recognize these IDs as valid values. I tried using typeof routesObject
, but nope, it didn’t work. After hours of searching the TypeScript docs (not great docs, by the way), tons of Stack Overflow pages, and even brainstorming with ChatGPT, I was getting nowhere.
That’s when the new idea hit: What if I just generate a TypeScript type file dynamically?
Boom. New plan:
- Extract all routes into an object.
- Generate a TypeScript type file dynamically.
- Run a script whenever routes change.
I wrote a script:
npm run generate-router-types
Which would output:
export type AppRoute = 'admin-projects-list' | 'home';
💥 Mission accomplished!
I pushed the PR, took a coffee break, and came back expecting a quick approval.
The "Joe" Moment 😐
Then I saw Joe’s comment. Let’s call him Joe (not his real name, but let’s roll with it).
"I have a better to do this, using only typescript"
My soul left my body. 💀
Bro, do you think I didn’t try that first?!
Then he messaged me directly:
"As-tu 15min pour discuter ta PR ?"
Great. A meeting. With Joe.
Joe was new at the company, but he had a crazy resume—Rust, Go, Node, DevOps, everything. But he also had that energy—the kind of dev who always has a counterpoint to everything.
Still, I joined the call with a whatever attitude. Let’s hear this genius out.
The Humiliation 🤡
Joe shared his screen.
He started explaining.
I saw his code.
And in that moment, I realized… I am, in fact, a junior. 😂
His solution? Pure TypeScript magic.
- No build scripts.
- No external tools.
- No route extraction function.
- Just a few lines of TypeScript.
type MergePath<T extends string | undefined, P extends string> =
T extends ""
? P
: P extends "/"|""
? `${P}${T}`
: `${P}/${T}`;
type ExtractRouteType<T extends RouteObject, P extends string> =
T extends { path: string } ?
{ route: P, paramsKeys: ExtractPathParams<P> }
: never;
type ExtractRoute<T extends RouteObject, P extends string> = (T extends { id: string, path: string } ? { [K in T["id"]]: ExtractRouteType<T, P> } : {}) & ExtractRoutes<T["children"], P>;
type ExtractRoutes<T extends RouteObject[] | undefined, P extends string = ""> = T extends [infer R extends RouteObject, ...infer S extends RouteObject[]] ? ExtractRoute<R, MergePath<R["path"], P>> & ExtractRoutes<S, P> : {};
export type RouteById = ExtractRoutes<typeof routeObjects> extends infer U ? { [K in keyof U]: U[K] } : never;
😭😭😭
I had spent hours on a solution that wasn’t even needed.
Joe laughed and said, "Yeah, TypeScript is really powerful when you know how to use it."
Yeah, I got schooled.
The Redemption Arc 🦸♂️
But instead of sulking, I fought back. I took Joe’s TypeScript wizardry and improved on it.
I thought: If he can dynamically extract routes, why can't I also dynamically validate parameters?
I wanted TypeScript to enforce route parameters, meaning:
✔ If a route requires :projectId
, I should be forced to pass { projectId: string }
as params.
✔ If I forgot a param, TypeScript should throw an error.
✔ If I added extra params that weren’t needed, TypeScript should complain.
✔ And of course, autocomplete should still work flawlessly.
I went back to my battle station. 💻☕
I introduced ExtractPathParams
, a TypeScript utility to extract only the required parameters from route paths:
type ExtractPathParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? [Param, ...ExtractPathParams<`/${Rest}`>]
: Path extends `${string}:${infer Param}`
? [Param]
: [];
Then, I rewrote buildRoute
so that it refused to compile if required parameters weren’t passed:
export function buildRoute<RouteId extends keyof RouteById>(
routeId: RouteId,
...params: ParamsArg<RouteId> extends undefined ? [] : [ParamsArg<RouteId>]
): string {
if (!routes[routeId]) {
throw new Error(`Route ${routeId} not found`);
}
let route = routes[routeId].route as string;
let params_ = params.length > 0 ? params[0] : {};
for (const [key, value] of Object.entries(params_)) {
route = route.replace(`:${key}`, value as string);
}
if (route.includes(':')) {
throw new Error(`Route ${routeId} contains an unresolved parameter`);
}
return route;
}
Boom. Now TypeScript would scream at me if I passed the wrong params! 🎯
I also refactored AppLink
to integrate this new validation:
const AppLink = <RouteId extends keyof RouteById>({
routeId,
...props
}: RouteBuilderProps<RouteId> & Omit<LinkProps, "to"> & React.RefAttributes<HTMLAnchorElement>) => {
return <Link to={buildRoute(routeId, props.params)} {...props}/>;
};
export default AppLink;
I ran the tests. Everything worked.
I opened another PR. This time, Joe actually liked it.
"Nice, I like this. Makes sure routes are truly type-safe. Good job."
😭 Sweet validation.
Now, instead of writing:
<Link to="/projects/123/groups/456">View Group</Link>
We could write:
<AppLink routeId="admin-project-group-consents" params={{ projectId: "123", groupId: "456" }}>
View Group
</AppLink>
✔ No more incorrect URLs.
✔ TypeScript-enforced validation.
✔ Autocomplete for route names & parameters.
✔ Less headache when changing paths in the future.
And just like that, the routing system was finally bulletproof. 🔥
The Lesson 🎓
That day, I learned: experience actually matters.
I could’ve held a grudge. I could’ve been bitter. But instead, I learned, improved, and built upon the knowledge.
The truth is, experience isn’t just about time spent coding. It’s about how deeply you challenge yourself, how far you push your skills, and how much you truly understand your tools.
That PR was a huge lesson for me. It wasn’t just about routing, or TypeScript, or Joe being a TypeScript wizard. It was about growth.
So if you ever find yourself humbled by a better solution, don’t resist it. Study it, build upon it, and make it your own.
That’s what makes a truly great developer. 😎
The End.
Top comments (27)
I'd argue that most junior part of this story is spending hours over hubris. Every dept/shop/team is different, but I tell my team if it takes more than hour, ask for guidance. Our budget has a limit.
To be fair that's definitely not junior typescript stuff hahaha. If you can write generic types like that without blinking a few times, I'd say you're an absolute typescript wizard.
Completely agree. Nothing in here is junior level.
Thx for a great story. I am arguing the typescript "wizardry" title. Because if we don't know how to use typescript to declare our special type with generic and condition, is just means one thing: we just put on hype but don't spend enough energy to know to use it. Most of typescript / JSDoc ( my favorite ) post are stop the explaining on basic types using mainly: string & number. But a ugly fact is our type ( this is a reall great example, thx again! ) are much-much more complex.
May I try to organize TS code a bit readable format.
This format is always working to me with a
?
&:
Have being working with TS in angular professionally, honojs and Nestjs recently. But TS generic codes like this still aches my brain.
Thanks.
Ok thanks for formatting this lol. Now this makes more sense without having to double take at what I was looking at
I don't see Joe as a senior dev. Sorry.
Being a code wizard/acrobat/whatever you want to call it, does not automatically make you a senior dev.
A senior dev should know better than to force their acrobatics and wizardry code onto a project, because noone will be able maintain that code in the future ...unless you have a team of only wizards, which never works because mosts wizards have an ego that's a bit** to work with.
(I'm talking specifically about web-application-development teams and not about wizards of the like that work at places like DeepMind)
I've seen this countless times that someone with exceptional coding skills joins a company/project but then fails to improve anything because they just make life harder for everyone by introducing wizardry and writing fancier code instead of actually trying to figure out how to improve the output and quality of the project by incorporating the whole team and knowing their skillsets.
And yes this is partially also a failure of management, but you can blame a manager only so much, when most of us assume: "Oh he's a wizard, he will definitely make the project and codebase better!"
Edit: That being said, i can perfectly relate to where you are coming from as i had the same thinking in the early days.
And you are a junior, but not because you cannot write fancy code (yet) - but because you think that fancy code is good code.
I scrolled hoping to see this comment.
This could have been solved with the same output a lot simpler in many ways.
This is just overengineering at its finest.
Being senior is actually writing simple, understandable and maintainable code. Not this "wizardry".
Thanks a lot <3 I'm really happy to hear that those leaks of Wizard skills doesn't keep me from becoming a Senior ^.^
Wow, your comment is extremely spot on in so many ways. It was a pleasure reading it.
To be fair, most of the stuff that's happening here is pretty yucky and it's almost like React goes out of its way to make things as convoluted as possible. Most people would probably come out with pretty hairy solutions!
Pure gold! Thank you, kind sir.
By the way, just a comment: I am a software engineer and developer, have been for several decades, but I'm also a writer and editor, and I just want to praise you're writing. Very tight. Very nice. Brilliantly explicative and evocative. Kudos.
I thought this was a good piece about the dev-life, if you will. It was also personally encouraging. But, most of all I thrilled at a legitimate experience of aesthetic pleasure where I was most certainly not expecting it. I forget which (I want to say modern, if not contemporary) poet said a main object of his craft was to "surprise and delight" his audience.
Thanks a lot, I'm really happy to hear that ^_^
Just use tanstack router
The param type matching was a nice touch, but the other types seem way over engineered. An actual Jr is going to have a to maintain this in a couple years. Why not just use an enum where the value is the URL and the name is the id?
const ROUTES = {
RouteID: URL
} as const
Then just replace all the URLs with the const value update your const object for the client request
Good post, buddy! Sounds like a fun experience to improve our TypeScript skills - I couldn't do it myself, not without struggling real bad first.
But honestly - in the real world? The Seniors in my team wouldn't even spend any time building it. They rather go with simpler, maintainable code, and prefer to focus on real problems.
I'm literally experiencing this now. Today lol. It's amazing that this post showed on my timeline. I'm also using it as a learning experience. But yeah, that initial feeling of " man, I just did this"😁 to"oh,I could've just done.....😑"
Some comments may only be visible to logged-in visitors. Sign in to view all comments.