akheron / typera

Type-safe routes for Express and Koa
MIT License
174 stars 14 forks source link

`Parser.query` order affects type conversion #862

Open mrsekut opened 2 weeks ago

mrsekut commented 2 weeks ago

When using typera-express with io-ts, we encountered a situation where the order in which Parser.query is applied seems to affect the type conversion of query parameters.

For reference, we are using the following types for the query parameters:

const Category = t.string; // Represents a category as a string
const PagingParams = t.type({
  page: IntFromString,        // Page number, expected to be converted to a number
  itemsPerPage: IntFromString // Items per page, also expected to be converted to a number
});

Below are examples of three patterns that demonstrate the issue:

  1. Pattern 1 (Working as expected): Using t.intersection to combine category and PagingParams works as intended. Both page and itemsPerPage are correctly converted to numbers.
const getWithIntersection = route
  .get('/test1')
  .use(Parser.query(t.intersection([t.type({ category: t.string }), PagingParams])))
  .handler(async req => {
    console.log(req.query); // { category: 'books', page: 1, itemsPerPage: 10 } Correct conversion
    return Response.ok(req.query);
  });
  1. Pattern 2 (Working as expected): Applying Parser.query separately, first with category and then with PagingParams, also results in correct conversion. Both page and itemsPerPage are properly handled as numbers.
const getWithSeparateUse = route
  .get('/test2')
  .use(Parser.query(t.type({ category: t.string })))
  .use(Parser.query(PagingParams))
  .handler(async req => {
    console.log(req.query); // { category: 'books', page: 1, itemsPerPage: 10 } Correct conversion
    return Response.ok(req.query);
  });
  1. Pattern 3 (Issue): However, when Parser.query(PagingParams) is placed before Parser.query(t.type({ category: t.string })), the page and itemsPerPage parameters remain as strings instead of being converted to numbers. This behavior is unexpected, given that both patterns should logically result in the same output.
const getWithSeparateUse2 = route
  .get('/test3')
  .use(Parser.query(PagingParams)) // Applied first
  .use(Parser.query(t.type({ category: t.string })))
  .handler(async req => {
    console.log(req.query); // { category: 'books', page: '1', itemsPerPage: '10' } page and itemsPerPage remain strings
    return Response.ok(req.query);
  });

Expected Behavior:

In all cases, page and itemsPerPage should be converted to numbers, as they are defined using IntFromString. However, the current behavior suggests that the order of applying Parser.query influences whether the conversion happens correctly, which can be surprising for developers. Since both the approach used in test2 and test3 are valid and commonly used, it would be more intuitive if they behaved consistently, regardless of the order in which the parsers are applied.