Factory function to create type-safe path builders that work with auto-generated route type definitions. This is designed to work with the vite-plugin’s generateTypedRoutes option.
Signature
function createTypedPaths<
TRoutes extends Record<string, Record<string, string>>
>(): TypedPaths<TRoutes>
Parameters
TRoutes
Record<string, Record<string, string>>
required
Type parameter representing the mapping of route paths to their parameter types.This is typically auto-generated by the vite-plugin as RouteParams.
Return Type
An object with a for() method for building type-safe paths.Build a URL path with type-checked parameters.Parameters:
path: string - Route path template (can include ?query suffix)
params?: Record<string, string> - Path parameters (required if route has dynamic segments)
Returns: Resolved URL path string
Examples
Basic Setup
// routes.generated.ts (auto-generated by vite-plugin)
import { createTypedPaths } from "@tailor-platform/app-shell";
type RouteParams = {
"/": {};
"/dashboard": {};
"/orders/:id": { id: string };
"/orders/:orderId/items/:itemId": { orderId: string; itemId: string };
};
export const paths = createTypedPaths<RouteParams>();
Static Routes
import { paths } from "./routes.generated";
// Static route - no params needed
paths.for("/dashboard"); // → "/dashboard"
paths.for("/"); // → "/"
Dynamic Routes
import { paths } from "./routes.generated";
// Dynamic route - params required and type-checked
paths.for("/orders/:id", { id: "123" }); // → "/orders/123"
// Multiple dynamic segments
paths.for("/orders/:orderId/items/:itemId", {
orderId: "456",
itemId: "789"
}); // → "/orders/456/items/789"
Routes with Query Strings
import { paths } from "./routes.generated";
// Static route with query string
paths.for("/dashboard?tab=overview"); // → "/dashboard?tab=overview"
// Dynamic route with query string
paths.for("/orders/:id?tab=details", { id: "123" });
// → "/orders/123?tab=details"
paths.for("/items/:id?tab=details&view=compact", { id: "456" });
// → "/items/456?tab=details&view=compact"
URL Encoding
import { paths } from "./routes.generated";
// Dynamic segments are automatically URL-encoded
paths.for("/products/:id", { id: "foo bar" });
// → "/products/foo%20bar"
paths.for("/products/:id", { id: "special/chars?#" });
// → "/products/special%2Fchars%3F%23"
Catch-all Routes
// Type definition with catch-all segment
type RouteParams = {
"/docs/*path": { path: string };
};
const paths = createTypedPaths<RouteParams>();
// Catch-all segments preserve slashes, encode individual segments
paths.for("/docs/*path", { path: "api/reference/hooks" });
// → "/docs/api/reference/hooks"
paths.for("/docs/*path", { path: "guide/getting started" });
// → "/docs/guide/getting%20started"
Type Safety
import { paths } from "./routes.generated";
// ✅ Correct usage
paths.for("/dashboard");
paths.for("/orders/:id", { id: "123" });
// ❌ TypeScript errors
paths.for("/orders/:id");
// Error: params required
paths.for("/orders/:id", { orderId: "123" });
// Error: wrong param name (should be 'id')
paths.for("/invalid");
// Error: path not found in RouteParams
Usage in Navigation
import { Link } from "react-router";
import { paths } from "./routes.generated";
const OrdersList = ({ orders }: { orders: Order[] }) => {
return (
<div>
{orders.map((order) => (
<Link
key={order.id}
to={paths.for("/orders/:id", { id: order.id })}
>
Order {order.id}
</Link>
))}
</div>
);
};
Programmatic Navigation
import { useNavigate } from "react-router";
import { paths } from "./routes.generated";
const ProductCard = ({ productId }: { productId: string }) => {
const navigate = useNavigate();
const handleClick = () => {
navigate(paths.for("/products/:id", { id: productId }));
};
return <button onClick={handleClick}>View Product</button>;
};
Building URLs from Data
import { paths } from "./routes.generated";
const buildOrderUrl = (orderId: string, itemId?: string) => {
if (itemId) {
return paths.for("/orders/:orderId/items/:itemId", {
orderId,
itemId
});
}
return paths.for("/orders/:id", { id: orderId });
};
// Usage
buildOrderUrl("123"); // → "/orders/123"
buildOrderUrl("123", "456"); // → "/orders/123/items/456"
TypeScript Types
// Extract base path (before ?) from a path with query string
type ExtractBasePath<T extends string> = T extends `${infer Base}?${string}`
? Base
: T;
// Check if an object type has no keys
type IsEmptyObject<T> = keyof T extends never ? true : false;
// Type-safe path builder interface
type TypedPaths<TRoutes extends Record<string, Record<string, string>>> = {
for<
T extends (keyof TRoutes & string) | `${keyof TRoutes & string}?${string}`,
>(
path: T,
...params: ExtractBasePath<T> extends keyof TRoutes
? IsEmptyObject<TRoutes[ExtractBasePath<T>]> extends true
? []
: [TRoutes[ExtractBasePath<T>]]
: never
): string;
};
Implementation Details
Path Parameter Replacement
The for() method replaces dynamic segments in the path template:
- Dynamic segments (
:paramName): Replaced with the encoded parameter value
- Catch-all segments (
*paramName): Replaced with the parameter value, encoding each path segment individually while preserving slashes
URL Encoding
- Dynamic segments (
:param) are encoded using encodeURIComponent(), which encodes the entire value including slashes
- Catch-all segments (
*param) split the value on /, encode each segment individually, then rejoin with / to preserve path structure
Notes
- This function is designed to work with auto-generated route types from the vite-plugin
- Path parameters are automatically URL-encoded
- Query strings can be included in the path template using
? syntax
- TypeScript will enforce that all required parameters are provided with correct types
- Empty parameter objects (
{}) indicate routes with no dynamic segments