Creates links type-safely.
This is distributed through a package registry called JSR.
NPM:
npx jsr add @kokomi/link-generator
PNPM:
pnpm dlx jsr add @kokomi/link-generator
Deno:
deno add @kokomi/link-generator
Yarn:
yarn dlx jsr add @kokomi/link-generator
Bun:
bunx jsr add @kokomi/link-generator
-
Define a route configuration object:
import { link_generator, type RouteConfig } from "@kokomi/link-generator"; const route_config = { home: { path: "/", }, users: { path: "/users", children: { user: { path: "/:id", }, }, }, } as const satisfies RouteConfig;
-
Create a generator:
const link = link_generator(route_config);
-
Generate links:
link("home"); // => '/' link("users"); // => '/users' link("users/user", { id: "alice" }); // => '/users/alice'
The link
function accepts a list of objects as query parameter generators
starting from the second argument onwards. In other words, the types of the
arguments from the second position onward in the link
function are any number
of Partial<Query>
types.
Moreover, if the object for setting query parameters contains an empty string or
an undefined
value, the link
function will not generate that query.
-
Create a generator:
const route_config = { products: { path: "/products?color", }, } as const satisfies RouteConfig; const link = link_generator(route_config);
-
Generate links:
link("products", undefined, { color: "red" }, { color: "blue" }); // => '/products?color=red&color=blue' link("products", undefined, { color: "" }, { color: undefined }); // => /products
The type of values for path and query parameters is string|number|boolean
by
default. While this is sufficient in most cases, this type can be made more
strict by defining a constraint area. This is a special string that can be
included in the path, like <Constraint>
. Conditions can be defined within open
(<
) and close (>
) mountain brackets. In this field, the following three type
constraints can be placed on path and query parameters:
-
String Type
You can narrow down the id to a string type by defining a condition field with a parameter name followed by the string
string
, as in/:id<string>
. -
Number Type
You can narrow down the id to a number type by defining a condition field with a parameter name followed by the string
number
, as in/:id<number>
. -
Boolean Type
You can narrow down the id to a boolean type by defining a condition field with a parameter name followed by the string
boolean
, as in/:id<boolean>
. -
Union Type
If you want to be strict and require that params and query only accept certain values other than string, number, and boolean, use the
<(Type1|Type2)>
syntax.The type of each segment of a union type defaults to its string literal type, but you can manually cast strings that can be converted to a
number
, such as 1, or toboolean
, such astrue
orfalse
, or strings that represent types such as string, number, or boolean. To do this, simply prepend*
to the string you want to cast, for example*123
,*true
, or*string
.
-
Create a generator:
const route_config = { user: { path: "/users/:id<string>", }, post: { path: "/post/:id<number>", }, category: { path: "/categories/:id<(a|*10|*false)>", }, news: { path: "/news?archived<boolean>", }, image: { path: "/image?width<(auto|*number)>", }, } as const satisfies RouteConfig; const link = link_generator(route_config);
-
Generate links:
link("user", { id: "alice" }); // Param type: { id: string } link("post", { id: 1 }); // Param type: { id: number } link("category", { id: "a" }); // Param type: { id: 'a' | 10 | false } link("news", undefined, { archived: true }); // Query type: { archived: boolean } link("image", undefined, { width: "auto" }); // Query type: { width: 'auto' | number }
If the link you want to generate contains a protocol, special type inference
needs to be done, so write the protocol and domain like this, with the protocol
ending in ://
and no /
before the domain:
const route_config = {
external: {
path: "https://",
children: {
youtube: {
path: "youtube.com",
children: {
video: {
path: "/watch?v",
},
},
},
},
},
} as const satisfies RouteConfig;
const link = link_generator(route_config);
link("external/youtube/video", undefined, { v: "123" });
// => 'https://youtube.com/watch?v=123'
The inferred type for each route can be obtained using the ExtractRouteData
type.
const route_config = {
user: {
path: "/users/:id",
},
news: {
path: "/news?archived<boolean>",
},
} as const satisfies RouteConfig;
type RouteData = ExtractRouteData<FlatRoutes<typeof route_config>>;
// {
// user: {
// path: "/users/:id";
// params: Record<"id", DefaultParamValue>;
// query: never;
// };
// news: {
// path: "/news";
// params: never;
// query: Record<"archived", boolean>;
// };
// }
Links are fragile, so calling them by unique route ids is essential instead of hard-coding them. To ensure the uniqueness of route ids while creating them efficiently, we use TypeScript's type checking with objects. Defining properties with the same name at the same level of an object will cause a type error, ensuring no overlapping route ids. Additionally, child route ids can be made unique by prefixing them with the parent route id.
const obj = {
route1: {},
// Type error! An object literal cannot have multiple properties with the same name.
route1: {},
};
By leveraging this, we can be confident that route ids do not overlap within the same level of the object. Moreover, the uniqueness of child route ids can be achieved by prefixing them with the parent route id. If the parent route id is unique, the child route ids will also be unique by necessity.
const obj = {
parent1: {
children: {
child1: {},
},
},
parent2: {
children: {
child1: {},
},
},
};
The generated route ids would be:
- parent1
- parent1/child1
- parent2
- parent2/child1
This approach allows flexible creation of route ids while maintaining a broad namespace.
This project was inspired by nanostores/router.
MIT