elysiajs / elysia

Ergonomic Framework for Humans
https://elysiajs.com
MIT License
10.64k stars 226 forks source link

Defaults values to not be typed as optional in the request handler #817

Open jasperdunn opened 2 months ago

jasperdunn commented 2 months ago

What is the problem this feature would solve?

When a default value is provided to an optional type, does it make sense for the request handler to know that it can't be undefined anymore, since we have provided a default?

The client of course should still see the type as optional.

Code:

const app = new Elysia()
  .get(
    '/qtest',
    ({ query }) => {
      console.log('Query:', query);
      return {
        query,
      };
    },
    {
      query: t.Object({
        pageNum: t.Optional(t.Numeric({ default: 1 })),
        pageSize: t.Optional(t.Numeric({ default: 10 })),
      }),
    }
  )
  .listen(2000);

app
  .handle(new Request('http://localhost:2000/qtest'))
  .then((x) => x.json())
  .then((x) => console.log('Response:', x));

Console output:

Query: {
  pageNum: 1,
  pageSize: 10,
}
Response: {
  query: {
    pageNum: 1,
    pageSize: 10,
  },
}

Screenshot 2024-09-08 at 11 11 13 AM Should the query be typed like this instead? Since default has been provided for these properties.

query: {
  pageNum: number;
  pageSize: number;
}

What is the feature you are proposing to solve the problem?

To change the way that types behave in the request handler, when default values are provided for Optional types.

What alternatives have you considered?

No response

SupertigerDev commented 2 months ago

What if you don't add t.Optional?

jasperdunn commented 2 months ago

What if you don't add t.Optional?

Then it's required by the client.

I want it optional on the client and not undefined on the server (when a default is provided).

SupertigerDev commented 2 months ago

Ah I see. This may be very hard or impossible to fix for them, thats my opinion. The dirty solution might be to use query.pageNum!

ebramanti commented 3 days ago

+1 to this issue, I am running into it as well for body parameters.

I think this might be solvable by overriding the behavior of .static to introspect fields that have default set. If they do, the key should not be optional.

Another option is to allow an Elysia-specific parameter for the value to be required at validation time, such as { required: true }. Then the user would either have to set this value in a transform handler or add a default.

ebramanti commented 3 days ago

I was able to find a workaround for this issue using Transform from Typebox.

Given this validation body as an example:

export const RequestBody = t.Object({
  givenArguments: t
    .Transform(t.Optional(t.Record(t.String(), t.Unknown())))
    .Decode((value) => value ?? {})
    .Encode((v) => v),
});

It will do the following:

It will produce the following output in docs (no "required" text means optional):

image

And the type from the body will be:

image
ebramanti commented 3 days ago

Here is how it can be exported as a reusable function with type safety:

import { t, type Static, type TSchema } from "elysia";
export const optionalWithDefaultValue = <T extends TSchema>(schema: T, defaultValue: Static<T>) =>
  t
    .Transform(t.Optional(schema))
    .Decode((value) => value ?? defaultValue)
    .Encode((value) => value);

And in the schema example:

export const RequestBody = t.Object({
  givenArguments: optionalWithDefaultValue(t.Record(t.String(), t.Unknown()), {}),
});
ebramanti commented 1 day ago

@jasperdunn The above did not work after all, I am now doing this:

t.Record(t.String(), t.Unknown(), { default: {} })

This seems to work for me, but it still shows the "required" text in the generated Swagger docs. I don't think there is a way to get around this, and wrapping a Transform in Optional will skip execution of the data transformation that would normally set the value to the default