Skip to content

Releases: cat394/link-generator

v9.0.0

28 Oct 03:30
Compare
Choose a tag to compare

What's Changed?

  • Removal of create_link_generator Function

    The create_link_generator function has been removed and replaced by a new function, link_generator. The link_generator function now accepts a route_config object, internally calls the flatten_route_config function, and transforms it into a Map. This approach allows for high-speed link generation through the returned link 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 Function

    The flatten_route_config function was previously public because it enabled easy visual representation of the flattened types while generating the link function in create_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 the flatten_route_config function. Since the link_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 Objects

    The previous link function had a limitation where multiple identical query parameters could not be generated. This has been resolved in version 9, and the link 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

16 Oct 16:31
Compare
Choose a tag to compare

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

15 Oct 04:35
Compare
Choose a tag to compare

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:

  1. Single Type Constraint

    "/users/:id<string>" // => { id: string }
  2. 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🎉

10 Sep 09:00
Compare
Choose a tag to compare

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:

  1. Transitioned the naming convention from camelCase to snake_case.

  2. The query property of the ExtractRouteData type has been renamed to queries, and the path property now infers more precise values.

  3. 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.

  4. Passing a value of 0 to a path parameter no longer omits that parameter.

Detailed Explanation

  1. 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

  2. Improvements to ExtractRouteData

    The path property of the ExtractRouteData 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
  3. 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.
  4. 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 with query.

    • Type names starting with Exclude have been changed to start with Remove.

v6.0.0

27 Aug 01:15
Compare
Choose a tag to compare

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

06 Aug 07:03
Compare
Choose a tag to compare

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:

  1. 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 😊
  2. 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

27 Jul 00:24
Compare
Choose a tag to compare

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

26 Jul 09:00
Compare
Choose a tag to compare

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

  1. 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'
 * }
 * */
  1. 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

  1. 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
  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 the FlatRoutes type. This represents the return value of flatRouteConfig 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

22 Jul 23:55
Compare
Choose a tag to compare

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

  1. 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 /.

  2. 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

18 Jul 05:58
Compare
Choose a tag to compare

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