Releases: cat394/link-generator
v9.0.0
What's Changed?
-
Removal of
create_link_generator
FunctionThe
create_link_generator
function has been removed and replaced by a new function,link_generator
. Thelink_generator
function now accepts aroute_config
object, internally calls theflatten_route_config
function, and transforms it into aMap
. This approach allows for high-speed link generation through the returnedlink
function.const route_config = { products: { path: "/products", }, } as const satisfies RouteConfig; const link = link_generator(route_config); link("products"); // => '/products'
-
Deprecation of
flatten_route_config
FunctionThe
flatten_route_config
function was previously public because it enabled easy visual representation of the flattened types while generating thelink
function 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_config
function. Since thelink_generator
function now calls this internally in version 9, there is no longer a need to keep it public. -
Modification of
link
Function API to Accept Any Number of Query ObjectsThe previous
link
function had a limitation where multiple identical query parameters could not be generated. This has been resolved in version 9, and thelink
function 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
query
property of theExtractRouteData
type has been renamed toqueries
, and thepath
property now infers more precise values. -
All properties in the third argument of the
link
function are now optional, and the use of?
to mark parameters as optional has been removed. -
Passing a value of
0
to 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
ExtractRouteData
The
path
property of theExtractRouteData
type 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
link
function.// 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
0
to 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
/:param
in a route such as/route/:param
with actual values, the/:param
part 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
searchParams
withquery
. -
Type names starting with
Exclude
have been changed to start withRemove
.
-
v6.0.0
What's changed?
- Renamed the search property in
ExtractRouteData
to query. - Updated the path property in the
ExtractRouteData
type 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
params
was 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
params
and 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' }); // => /products
In 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/1
This 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 OK
However, 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
FlattenRouteConfig
type is no longer published. Instead, use theFlatRoutes
type. This represents the return value offlatRouteConfig
with 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
false
as 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.
v3.0.0
Breaking Change!!
We've changed the way absolute paths are handled, greatly simplifying the code implementation and resulting in a much improved developer experience and performance.
Migrating from Version 2
To migrate from version 2.0.0 to version 3.0.0, prepend /
to all paths in your RouteConfig. This change enhances consistency and clarity in defining routes.
Why this change?
In previous versions, there was inconsistency in how paths were defined. Some paths started with /
while others didn't, leading to confusion. Version 3.0.0 standardizes this by requiring all paths to be absolute, making the route definitions more intuitive and easier to understand.
Migration Steps
-
Update Route Definitions
In version 3.0.0, ensure all paths start with
/
.Before version 2.0.0:
const routeConfig = { home: { path: '/' }, users: { path: 'users/:userid' } products: { path: 'products', children: { search: { path: 'search/?size&color' } } } } as const satisfies RouteConfig;
Version 3.0.0:
const routeConfig = { home: { path: '/', }, users: { path: '/users/:userid', }, products: { path: '/products', children: { search: { path: '/search/?size&color', }, }, }, } as const satisfies RouteConfig;
Now, all paths are prefixed with
/
. -
Handling Absolute Paths
This migration is optional and will continue to work as expected with previous versions.
In previous versions, absolute paths required the Route ID to be prefixed with
*
. In version 3.0.0, this is no longer necessary, though the*
prefix will still work for compatibility.Before version 2.0.0:
const routeConfig = { '*external': { path: 'https://', children: { youtube: { path: 'youtube.com', children: { watch: '/watch?videoid', }, }, }, }, };
Version 3.0.0:
const routeConfig = { 'external': { path: 'https://', children: { youtube: { path: 'youtube.com', children: { watch: '/watch?videoid', }, }, }, }, };
Summary
Make sure you precede all relative paths in RouteConfig with a '/'.
v2.0.0
Breaking Change!!
Due to my lack of understanding of query parameters, I used to generate the query parameters with the characters ?q=
prepended, but I realized this was wrong and changed it to start with ?
.
Example:
Previous:
const routeConfig = {
productsSearch: {
path: 'products/search/?q=size'
}
}
// ... create link generator
link('productsSearch', null, { size: 'large' }) // result: '/products/search?q=size'
Now:
const routeConfig = {
productsSearch: {
path: 'products/search/?size'
}
}
link('productsSearch', null, { size: 'large'}) // result: '/products/search?size=large