@plainbrew/next-typed-href
v0.6.0
Published
Type-safe href generator for Next.js App Router
Maintainers
Readme
@plainbrew/next-typed-href
Type-safe href generator for Next.js App Router.
Examples
- example-next-typed-href-nuqs — minimal Next.js App Router example using
defineTypedHrefWithNuqswith nuqs
Install
pnpm add @plainbrew/next-typed-hrefSetup
Define your routes and params map, then create a $href function:
lib/href.ts:
import { defineTypedHref } from "@plainbrew/next-typed-href";
import type { AppRoutes, ParamsOf } from "@/../.next/types/routes";
type AppRouteParamsMap = { [Route in AppRoutes]: ParamsOf<Route> };
export const { $href } = defineTypedHref.routes<AppRoutes, AppRouteParamsMap>();Usage
import { $href } from "@/lib/href";
// Static route
$href({ route: "/" });
// => "/"
// Dynamic segment
$href({ route: "/users/[id]", routeParams: { id: "42" } });
// => "/users/42"
// With search params
$href({ route: "/users", searchParams: { page: "2" } });
// => "/users?page=2"
// With hash
$href({ route: "/users/[id]", routeParams: { id: "1" }, hash: "profile" });
// => "/users/1#profile"
// Catch-all segment
$href({ route: "/posts/[...slug]", routeParams: { slug: ["2024", "hello"] } });
// => "/posts/2024/hello"Supported segment types
| Segment | Example route | routeParams type |
| ------------------ | ------------------- | --------------------- |
| Dynamic | /users/[id] | { id: string } |
| Catch-all | /posts/[...slug] | { slug: string[] } |
| Optional catch-all | /docs/[[...path]] | { path?: string[] } |
Notes
- All param values are automatically URL-encoded.
searchParamsaccepts anything thatURLSearchParamsaccepts (plain object, array of pairs, orURLSearchParamsinstance).hashshould be specified without the leading#.- Optional catch-all segments (
[[...param]]) resolve to an empty string whenundefinedis passed.
TypedHref — branded return type
By default $href() returns string. Pass { branded: true } to .withOptions() to get a TypedHref branded type instead, which lets you distinguish $href() output from plain strings at the type level.
import { defineTypedHref } from "@plainbrew/next-typed-href";
import type { TypedHref } from "@plainbrew/next-typed-href";
export const { $href } = defineTypedHref
.routes<AppRoutes, AppRouteParamsMap>()
.withOptions({ branded: true });import { $href } from "@/lib/href";
import type { TypedHref } from "@plainbrew/next-typed-href";
type LinkProps = { href: TypedHref };
function SafeLink({ href }: LinkProps) { ... }
// ✓ $href() result passes through
<SafeLink href={$href({ route: "/" })} />
// ✗ plain string causes a compile error
<SafeLink href="/" />TypedHref is a subtype of string, so it can be passed anywhere a string is expected — existing code is unaffected.
nuqs integration
For routes with typed search params powered by nuqs, use the ./nuqs entry point:
pnpm add @plainbrew/next-typed-href nuqslib/href.ts:
import { defineTypedHrefWithNuqs } from "@plainbrew/next-typed-href/nuqs";
import { parseAsInteger, parseAsString } from "nuqs/server";
import type { AppRoutes, ParamsOf } from "@/../.next/types/routes";
type AppRouteParamsMap = { [Route in AppRoutes]: ParamsOf<Route> };
export const { $href } = defineTypedHrefWithNuqs.routes<AppRoutes, AppRouteParamsMap>().nuqs({
"/search": {
q: parseAsString,
page: parseAsInteger,
},
});Usage
import { $href } from "@/lib/href";
// nuqs-typed search params
$href({ route: "/search", searchParams: { q: "hello", page: 2 } });
// => "/search?q=hello&page=2"
// null / undefined values are omitted
$href({ route: "/search", searchParams: { q: "hello", page: null } });
// => "/search?q=hello"
// Routes without parsers fall back to standard URLSearchParams
$href({ route: "/posts", searchParams: { page: "1" } });
// => "/posts?page=1"
// Dynamic segment + nuqs search params
$href({ route: "/users/[id]", routeParams: { id: "42" }, searchParams: { tab: "profile" } });
// => "/users/42?tab=profile"withDefault pattern
Parsers wrapped with .withDefault() make the type non-nullable and omit the key from the URL when the value equals the default:
export const { $href } = defineTypedHrefWithNuqs.routes<AppRoutes, AppRouteParamsMap>().nuqs({
"/search": {
q: parseAsString.withDefault(""),
page: parseAsInteger.withDefault(1),
},
});
// Value differs from default → included
$href({ route: "/search", searchParams: { q: "hello", page: 2 } });
// => "/search?q=hello&page=2"
// Value equals default → omitted (consistent with nuqs URL semantics)
$href({ route: "/search", searchParams: { q: "hello", page: 1 } });
// => "/search?q=hello"
// null is a type error for withDefault params
$href({ route: "/search", searchParams: { q: null } });
// => TypeError: Type 'null' is not assignable to type 'string | undefined'withOptions — shared builder options
.withOptions() lets you configure behavior for the entire builder. Call it between .routes() and .nuqs(). Calling .withOptions() multiple times replaces the previous options.
requiredSearchParams
When true, searchParams becomes required on routes that have nuqs parsers defined:
- Fields without
.withDefault()→ required (the type includesnull, sonullis accepted) - Fields with
.withDefault()→ optional
export const { $href } = defineTypedHrefWithNuqs
.routes<AppRoutes, AppRouteParamsMap>()
.withOptions({ requiredSearchParams: true })
.nuqs({
"/search": {
q: parseAsString, // required (no withDefault)
page: parseAsInteger.withDefault(1), // optional (has withDefault)
},
});
$href({ route: "/search", searchParams: { q: "hello" } }); // OK
$href({ route: "/search" }); // Type error: searchParams is required
$href({ route: "/search", searchParams: { page: 2 } }); // Type error: q is requiredbranded
Returns TypedHref instead of string — see TypedHref above.
nuqs integration notes
nullandundefinedvalues are omitted from the query string.- Values are serialized using nuqs'
createSerializer, which respectswithDefaultbehavior. - When a
withDefaultparam value equals the default, the key is cleared from the URL. nullis a type error forwithDefaultparams (the type is non-nullable).- Routes without a parser defined fall back to standard
URLSearchParamsbehavior. nuqsis a peer dependency and is optional for projects not using this entry point.
