Releases: cat394/link-generator
v9.2.0
Changes:
Remove redundant runtime error handling
The API has always been type-safe, and this error case was already very unlikely.
The explicit runtime error was originally added mainly as a small convenience for JavaScript users.
Since TypeScript users would reliably hit a type error anyway, the extra check felt unnecessary, so it has been removed to keep the implementation simpler.
v9.1.0
What's changed?
🚀 New Features
🧩 should_append_query Option
A new should_append_query boolean flag has been introduced to control whether encoded query parameters are automatically appended to the generated link.
const route_config = {
products: {
path: "/products?order"
}
} as const satisfies RouteConfig;
const link = link_generator(route_config, { should_append_query: false });
link("products", undefined, { order: "asc" }); // => /products (no query string!)🔧 transform Option
You can now customize link generation behavior by passing a transform function to link_generator.
- The
transformfunction receives aRouteContextobject that provides detailed metadata about the current route, includingid,path,params, andquery. - It can return a custom
stringpath, or returnundefinedto fall back to the defaultctx.path. - This makes it easy to override only specific routes while preserving default behavior for the rest.
const link = link_generator(route_config, {
transform: (ctx) => {
const { id, path, params, query } = ctx;
if (id === "products" && query.order) {
return "/custom";
}
// fallback to ctx.path
},
should_append_query: false
});
link("products", undefined, { order: "asc" }); // => /custom
link("products"); // => /products (because no order query)📚 Documentation
Added full documentation for transform and add_query under the Options section.
Examples included for conditional transforms and query parameter control.
This is especially useful in combination with transform, where you might want full control over the output path.
v9.0.0
What's Changed?
-
Removal of
create_link_generatorFunctionThe
create_link_generatorfunction has been removed and replaced by a new function,link_generator. Thelink_generatorfunction now accepts aroute_configobject, internally calls theflatten_route_configfunction, and transforms it into aMap. This approach allows for high-speed link generation through the returnedlinkfunction.const route_config = { products: { path: "/products", }, } as const satisfies RouteConfig; const link = link_generator(route_config); link("products"); // => '/products'
-
Deprecation of
flatten_route_configFunctionThe
flatten_route_configfunction was previously public because it enabled easy visual representation of the flattened types while generating thelinkfunction increate_link_generator. It was also meant to save the effort of creating specific type definitions to obtain the flattened types. However, this restricted the ability to make breaking changes to theflatten_route_configfunction. Since thelink_generatorfunction now calls this internally in version 9, there is no longer a need to keep it public. -
Modification of
linkFunction API to Accept Any Number of Query ObjectsThe previous
linkfunction had a limitation where multiple identical query parameters could not be generated. This has been resolved in version 9, and thelinkfunction has been modified to accept any number of query objects starting from the third argument.const route_config = { products: { path: "/products?color&size", } } as const satisfies RouteConfig; const link = link_generator(route_config); link('products', undefined, { color: 'red' }, { color: 'blue' }, { color: 'green', size: 'small' }); // => /products?color=red&color=blue&color=green&size=small
-
Improved Code Readability
Variable and function names used internally have been clarified to enhance code readability.
v8.0.1
Improvements
The performance of type inference for union string types has been improved.
Before the improvement
Previously, union string types were split into an array of strings using the | operator, and type inference was performed on each union element one by one, manually checking whether each element needed type conversion. This method was inefficient, as it required sequentially extracting and processing each array element.
After the improvement
In this version, the approach has been changed to split string literal types and create a union type directly, and then perform type conversion on the union type as a whole. This eliminates the need to sequentially process each array element, significantly improving the performance of type inference.
v8.0.0
Enhanced Union String in the Constraint Area
The highlight of this milestone release, version 8, is the enhanced union strings in the constraint area!
Previously, we could apply two types of constraints to parameter types:
-
Single Type Constraint
"/users/:id<string>" // => { id: string }
-
Union Type Constraint with Literal Types
"/users/:id<(a|1|true)>" // => { id: "a" | 1 | true }
However, there were some type patterns that couldn't be achieved with this approach.
For example, you couldn’t create a union of primitive types like string|number. There may also be situations where you want to handle values like "123" or "true" as strings without automatic type conversion.
Unfortunately, this was not possible in v7. If you specified <(string|number)>, it would generate a union of string literals like "string"|"number".
To address this, in v8 we introduced manual type conversion support, allowing conversions to primitive types.
This transition is intuitive, simple, and extremely easy to implement!
The key thing to remember is to add * before the elements in the union string that need to be converted!
This means that any union string without the * prefix will be treated as a union of string literals.
Prior to v7
const route_config = {
route_1: {
path: "/:param<(string|number)>"
},
route_2: {
path: "/:param<(a|10|true)>"
}
} as const satisfies RouteConfig;
// ...create link generator
link("route_1", { param: "number" });
// Param type is { param: "string" | "number" }
link("route_2", { param: 10 });
// Param type is { param: "a" | 10 | true }From v8 onwards
const route_config = {
route_1: {
path: "/:param<(string|number)>" // No automatic type conversions
},
route_2: {
path: "/:param<(*string|*number)>"
},
route_3: {
path: "/:param<(abc|123|boolean)>" // No automatic type conversions
},
route_4: {
path: "/:param<(abc|*123|*boolean)>"
}
} as const satisfies RouteConfig;
// ...create link generator
link("route_1", { param: "number" });
// Param type is { param: "string" | "number" }
link("route_2", { param: 123 });
// Param type is { param: string | number }
link("route_3", { param: "boolean" });
// Param type is { param: "abc" | "123" | "boolean" }
link("route_4", { param: true });
// Param type is { param: "abc" | 123 | boolean }The only breaking change from v7 is this! Since it only affects type inference and does not change function implementations, you can migrate with confidence.
Other Improvements
- Resolved ambiguities in type inference.
- Clarified internal function names and variable names.
- Updated and revised the documentation for v8.
The memorable version 7🎉
Summary of Updates
This update includes a significant number of changes, effectively giving the link-generator a fresh start.
For those who may not want to go through all the details, here are the key highlights:
-
Transitioned the naming convention from camelCase to snake_case.
-
The
queryproperty of theExtractRouteDatatype has been renamed toqueries, and thepathproperty now infers more precise values. -
All properties in the third argument of the
linkfunction are now optional, and the use of?to mark parameters as optional has been removed. -
Passing a value of
0to a path parameter no longer omits that parameter.
Detailed Explanation
-
Transition from camelCase to snake_case
To improve consistency and readability, we have transitioned from camelCase to snake_case across the project.
We have rewritten the entire project using snake_case instead of camelCase.
The following function names have been updated:
-
flattenRouteConfig=>flatten_route_config -
createLinkGenerator=>create_link_generator
-
-
Improvements to
ExtractRouteDataThe
pathproperty of theExtractRouteDatatype has been improved, and now returns the value without the constraint area.const route_config = { route1: { path: "/:param<string>?key" } } as const; // Before const flat_route_config = flattenRouteConfig(route_config); type Route1Path = ExtractRouteData<typeof flat_route_config>["route1"]; // => /:param<string> // After Version 7 const flat_route_config = flatten_route_config(route_config); type RoutePath = ExtractRouteData<typeof flat_route_config>["route1"]; // => /:param
-
Removed the syntax for making query parameters optional using
?Since all query parameters are assumed to be optional, we have adjusted the types to reflect this.
As of version 7, the use of
?for optional query parameters has been removed.const route_config = { route1: { path: "/?key1&key2", }, } as const; // Before const flat_route_config = flattenRouteConfig(route_config); type Route1Query = ExtractRouteData<typeof flat_route_config>; // => { key1: DefaultParamValue, key2: DefaultParamValue } // After Version 7 const flat_route_config = flatten_route_config(route_config); type Route1Query = ExtractRouteData<typeof flat_route_config>; // => Partial<{ key1: DefaultParamValue, key2: DefaultParamValue }>
This change makes all query parameters optional when generating links with the
linkfunction.// Before link("route1", undefined, {}); // Type error! The query object must have key1 and key2 properties. // If you wanted all properties to be optional you had to add a "?" as a suffix to all query names. const optional_route_config = { route1: { path: "/?key1?&key2?", }, } as const; link("route1", undefined, {}); // After Version 7 // This uses the link function generated from the route_config mentioned above. link("route1", undefined, {}); // key1 and key2 are optional.
-
Bug Fixes Related to Path Parameters
Previously, passing a value of
0to a path parameter would result in that parameter being omitted.In version 7, this has been fixed, and the correct path is now generated.
const route_config = { route1: { path: "/:param" } } as const; // Before link("route1", { param: 0 }); // => "/" // After Version 7 link("route1", { param: 0 }); // => "/0"
Internal Changes
-
Documentation Update
In line with the changes above, we have rewritten all instances of camelCase in the documentation to snake_case.
Additionally, we have removed sections of the README that referred to the now-deprecated syntax for optional query parameters.
-
Test Updates
Previously, all tests were written in a single file. Recognizing that this was not scalable for testing multiple cases, we have split the tests into the following files:
-
path_abusolute.test.ts -
path_constraint.test.ts -
path_static.test.ts -
path_with_params_and_query.test.ts -
path_with_params.test.ts -
path_with_queries.test.ts
This improves scalability by allowing individual test cases to be managed separately.
-
-
File Name Updates
To make file names more intuitive, we have aligned them with their respective function names.
-
generator.ts=>create_link_generator.ts -
flatConfig.ts=>flatten_route_config.ts
-
-
Simplified Logic for Replacing Path Parameter Values
Previously, when replacing path parameters like
/:paramin a route such as/route/:paramwith actual values, the/:parampart was captured and replaced as/value. However, the initial/was unnecessary to match, so we have changed the logic to exclude the initial/using a positive lookahead. This is necessary to distinguish between port numbers and path parameters. -
Other Changes
-
We have unified variable and type names, such as replacing instances of
searchParamswithquery. -
Type names starting with
Excludehave been changed to start withRemove.
-
v6.0.0
What's changed?
- Renamed the search property in
ExtractRouteDatato query. - Updated the path property in the
ExtractRouteDatatype to remove query string from its value.
Example
-
Version 5
const routeConfig = { products: { path: "/products?size" } } as const satisfies RouteConfig; type FlatResult = FlatRoutes<typeof routeConfig>; type RouteData = ExtractRouteData<FlatResult>; type ProductsRoute = RouteData["products"]; /** * * { * path: "/products?size", * params: never, * search: { size: DefaultParamValue } * } * */
-
Version 6
const routeConfig = { products: { path: "/products?size" } } as const satisfies RouteConfig; type FlatResult = FlatRoutes<typeof routeConfig>; type RouteData = ExtractRouteData<FlatResult>; type ProductsRoute = RouteData["products"]; /** * * { * path: "/products", // NEW! Exclude query parts * params: never, * query: { size: DefaultParamValue } // NEW! search property -> query property * } * */
v5.0.0
Major Changes:
The most significant change in this version is that path parameters can no longer be optional. This update enforces stricter type checks for parameters and allows for intuitive and type-safe query parameter definitions.
Previous Issues:
We had two main issues with the previous version of this package:
-
Ensuring Required Path Parameters Are Set
Consider the following code example:
const routeConfig = { userPosts: { path: '/users/:userid/posts/:postid', } } as const satisfies RouteConfig; // ...create link function type UserPostsRouteData = ExtractRouteData<typeof flatRouteConfig>['userPosts']; /** * UserPostsRouteData = { * path: 'userPosts' // Incorrect type inference has also been fixed. * params: Record<'userid', DefaultParamValue> | Record<'postid', DefaultParamValue> * search: never; * } */ const userPostsLink = link('userPosts', { userid: '123' }); // No type errors occurred even if not all required parameters were set when generating a path. // => '/users/123/posts'
In situations with multiple path parameters, the type of
paramswas a union type of the parameters, which did not trigger type errors if not all required parameters were set. This was not the desired behavior.In version 5, this has been improved by making
paramsand search parameters an intersection type of each path parameter. Now, a type error occurs if all required path parameters are not set.// Version 5: type UserPostsRouteData = ExtractRouteData<typeof flatRouteConfig>['userPosts']; /** * UserPostsRouteData = { * path: 'userPosts' // Incorrect type inference has been fixed. * params: Record<'userid', DefaultParamValue> & Record<'postid', DefaultParamValue> * search: never; * } */ const userPostsLink = link('userPosts', { userid: '123' }); // Type error, postid parameter must be set. const userPostsLink = link('userPosts', { userid: '123', postid: 1 }); // Type-safe 😊
-
Query Parameters
Previously, it was possible to make path parameters optional, but this led to ambiguity. Consider the following route configuration:
const routeConfig = { users: { path: '/users/:userid?' } } as const satisfies RouteConfig; // ...create link function const usersLink = link('users'); const userLink = link('users', { userid: '123' });
This can be confusing because a single route ID generates different paths. Instead, it should be defined like this:
const routeConfig = { users: { path: '/user', children: { user: { path: '/:userid' } } } } as const satisfies RouteConfig; // ...create link function const usersLink = link('users'); const userLink = link('users/user', { userid: '123' });
While this structure might seem nested and less elegant, it enforces the rule that each route ID generates a single path. This ensures type safety for path parameters and flexibility for any extensions beyond
/user.Consequently, optional path parameters have been deprecated. Moreover, this change eliminates the need to prefix query parameters with
/, making route definitions more intuitive without requiring specific rules.Now, if a property is not marked as optional, all its values must be set, otherwise, a type error will occur.
const routeConfig = { categories: { path: '/categories?size&color' } } as const satisfies RouteConfig; // ...create link function const categoriesLink = link('categories', undefined, { size: 'small' }); // Type error, please set the color parameter. const categoriesLink = link('categories', undefined, { size: 'small', color: 'red' }); // Type-safe 😊
If you want a parameter to be optional, just add a
?after the parameter, as in previous versions.const routeConfig = { categories: { path: '/categories?size&color?' } } as const satisfies RouteConfig; // ...create link function const categoriesLink = link('categories', undefined, { size: 'small' }); // Type-safe 😊
Modification Details:
Previously, the path property of the ExtractRouteData type inferred the route ID. This has been corrected to reflect the actual path value of the route.
const routeConfig = {
categories: {
path: '/categories?size&color'
}
} as const satisfies RouteConfig;
const flatConfig = flattenRouteConfig(routeConfig);
type CategoriesRouteData = ExtractRouteData<typeof flatConfig>['categories'];
/**
* CategoriesRouteData = {
* path: 'categories', // The path property should be the actual path value of that route!
* params: never;
* search: Record<'size', DefaultParamValue> & Record<'color', DefaultParamValue>
* }
*/
// Version 5:
type CategoriesRouteData = ExtractRouteData<typeof flatConfig>['categories'];
/**
* CategoriesRouteData = {
* path: '/categories?size&color', // Correct!
* params: never;
* search: Record<'size', DefaultParamValue> & Record<'color', DefaultParamValue>
* }
*/Other Breaking Changes:
The ParamValue type is no longer exported. Instead, path parameters are typed using DefaultParamValue, and search parameters use Partial<DefaultParamValue>.
v4.0.1
Improve Type performace
We have revised the method for extracting search parameters from paths.
Let's take a simple example to illustrate this. Suppose we have the following path string literal type:
type Path = '/products/?size&color';Previously, we handled search parameters in the same way as path parameters. First, we split the path by /:
type Separated = ['', 'products', '?size&color'];Then, we extracted the segment starting with ? as the search parameter:
type SearchParamField = 'size&color';However, this approach was inefficient. Unlike path parameters, search parameters always appear at the end of the path. Therefore, we changed our approach to split the path string into the portion starting with /? and the rest.
type FindSearchParamField<T> = T extends `${infer Head}/?${SearchParamField}` ? SearchParamField : T;
type SearchParamField = FindSearchParam<Path>;
// ^
// 'size&color'v4.0.0
Announcement of Version 4.0.0
link-generator has finally reached the highest level.
All issues from previous versions have been resolved, with stricter typing and improved performance.
Here, we will explain the problems that previous versions had and what has changed from version 3.0.0 in two sections.
Previous Issues
- Handling Search Parameters
In previous versions, there was a problem with handling query parameters. This was caused by query parameters included in the parent route path being inherited by child routes.
For example, consider the following route configuration object:
const routeConfig = {
products: {
path: '/products/?size&color',
children: {
product: {
path: '/:productid'
}
}
}
} as const satisfies RouteConfig;In this case, the path for the child route is not generated. This is because the child route inherits the path of the parent route. When generating paths, the string /? is searched first, and if found, all query parameters after it are deleted. Therefore, previously only the last child route could have query parameters. This was completely incorrect and should have been tested.
link('products/product', { productid: '1' }); // => /productsIn version 4.0.0, this has been resolved, and child routes no longer inherit query parameters included in the parent route path.
link('products/product', { productid: '1' }); // => /products/1This change required the creation of additional types. Previously, the FlattedRouteConfig type allowed you to obtain the type of the flattened route configuration object, but from version 4.0.0 onward, please use the FlatRoutes type. This is not just a name change; a new type that removes query parameters included in the parent route path from FlattedRouteConfig has been implemented.
type FlatConfig = FlatRoutes<typeof routeConfig>;
/* ^
* {
* 'products': '/products?size&color',
* 'products/product': '/products/:productid'
* }
* */- Type Safety When Parameters Are Present
The link function's second and third arguments have been modified to be dynamically generated types. The previous link function kept the types simple by making the second and third arguments optional objects. This worked well for generating paths, but it was a problem that no type error occurred when generating paths requiring path parameters. This was one of the tasks I had been putting off. Dynamically generating function parameters with TypeScript is a bit tricky.
link('products/product'); // This does not cause a type error.However, from version 4.0.0, a type error occurs unless you pass the type of the parameters in the second argument for paths with defined parameters. Search parameters remain optional. This means that in paths that include search parameters, no type error will occur if you do not pass a search parameter object in the third argument.
link('products', undefined, { size: 'large' });
// or
link('products'); // No type error will occur if you do not set search parameters.Note that undefined is set as the second argument. Before version 3.0.0, you could set null if query parameters were present.
// Before version 3
// Both are OK
link('products', null, { size: 'large' });
link('products', undefined, { size: 'large' });However, from version 4 onward, if the path does not have path parameters and only includes search parameters, the second argument can only accept undefined.
link('products', null, { size: 'large' }); // ❌null is type error!
link('products', undefined, { size: 'large' }); // ✅Correct!These are the main changes in version 4!
Migration from Version 3
- Review Route Configuration Objects
Migration from version 3 to version 4 may require some changes to the route configuration object. This is because previous versions did not treat paths with parameters as mandatory. For example, let's say you wrote the route configuration object like this:
const routeConfig = {
users: "/users/:user"
} as const satisfies RouteConfig;In that case, the users route could generate two paths, /users and /users/some-userid, without causing a type error.
link('users'); // Type is OK
// or
link('users', { userid: '1' }); // Type is OKHowever, from version 4 onward, a type error occurs unless you explicitly set path parameters for paths with path parameters.
link('users'); // TypeError: Path parameters must be set!In this case, you need to create a separate route for paths with path parameters, as shown below.
const routeConfig = {
users: {
path: '/users',
children: {
user: {
path: '/:userid'
}
}
}
} as const satisfies RouteConfig;
link('users'); // => /users
link('users/user', { userid: '1' }); // => /users/1- Change of Type Names
You only need to read this section if you are building something using the types published by my package.
In version 4, types have been carefully reviewed one by one and renamed to more clear and understandable names.
- DefaultParameterType => DefaultParamValue
- Parameter => Param
- Param => PathParam
- The
FlattenRouteConfigtype is no longer published. Instead, use theFlatRoutestype. This represents the return value offlatRouteConfigwith the search parameters included in the parent route removed.
Others
The following information is about the bugs fixed and performance improvements in this update.
-
In versions prior to version 3, setting
falseas a path parameter caused that path parameter segment to be omitted, which has been fixed. -
Clear documentation has been added to the type definition files to help anyone understand the type inference being done in this package.