sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.98k stars 157 forks source link

Composite with Union with Literal(Number) not working #600

Closed rhkdgns95 closed 1 year ago

rhkdgns95 commented 1 year ago

package.json

"@sinclair/typebox": "^0.31.1",

Desribe

export const getBannersFilter = t.Composite([
  pagingFilterSchema,
  t. Object({
    visible: t.Optional(
      t.Union([t.Literal(0), t.Literal(1)], {
        error: "공개여부(visible: 1 | 0)값을 올바르게 입력해주세요",
      })
    ),
  }),
]);

If you configure the type verification keywords "Composite", "Union", and "Literal (Number type)" shown in the code above, it will not work.

However, Literal (String type) operates normally.

Please check the example below.

// [O] This works.
export const getBannersFilter = t.Composite([
  pagingFilterSchema,
  t. Object({
    visible: t.Optional(
      t.Union([t.Literal("0"), t.Literal("1")], {
        error: "공개여부(visible: "1" | "0")값을 올바르게 입력해주세요",
      })
    ),
  }),
]);
// [X] This causes an error.
export const getBannersFilter = t.Composite([
  pagingFilterSchema,
  t. Object({
    visible: t.Optional(
      t.Union([t.Literal(0), t.Literal(1)], {
        error: "공개여부(visible: 1 | 0)값을 올바르게 입력해주세요",
      })
    ),
  }),
]);
sinclairzx81 commented 1 year ago

@rhkdgns95 Hi,

I can't seem to reproduce an error, however the current TypeBox version I'm running is 0.31.15 so you may want to install the latest revision. The following seems to compile and infer as expected.

TypeScript Link Here

import { Type, Static } from '@sinclair/typebox'

export const pagingFilterSchema = Type.Object({
  query: Type.String(),
  skip: Type.Number(),
  take: Type.Number()
})

export const getBannersFilter = Type.Composite([
  pagingFilterSchema,
  Type.Object({
    // this is fine....
    visible1: Type.Optional(
      Type.Union([Type.Literal(0), Type.Literal(1)])
    ),
    visible2: Type.Optional(
      Type.Union([Type.Literal('0'), Type.Literal('1')])
    ),
  }),
])

type T = Static<typeof getBannersFilter>

I don't believe there has been any changes to Composite in the 0.31.x release. What error do you see? Cheers S

rhkdgns95 commented 1 year ago

@sinclairzx81 Thank you for answer!

I am using elysia.

This package is compatible with version 0.31.1.

Again, compilation runs normally as shown above.

However, verification consistently fails. In numeric formats such as

export const getBannersFilter = Type.Composite([
  pagingFilterSchema,
  Type.Object({
    // this is always failed
    visible1: Type.Optional(
      Type.Union([Type.Literal(0), Type.Literal(1)])
    ),
    // this is fine...
    visible2: Type.Optional(
      Type.Union([Type.Literal('0'), Type.Literal('1')])
    ),
  }),
])
sinclairzx81 commented 1 year ago

@rhkdgns95 Hi, this might be more of a question for the Elysia project, but have tested against the latest Elysia (so, fresh Bun install and fresh Elysia app with bun create elysia app) and things seem to be working ok.

The following is the test I'm running locally.

// -----------------------------------------------------------------------
// Server: bun create elysia app 
// -----------------------------------------------------------------------

import { Elysia, t } from "elysia";

export const pagingFilterSchema = t.Object({
  query: t.String(),
  skip: t.Number(),
  take: t.Number()
})
export const getBannersFilter = t.Composite([
  pagingFilterSchema,
  t.Object({
    visible1: t.Optional(
      t.Union([t.Literal(0), t.Literal(1)])
    ),
    visible2: t.Optional(
      t.Union([t.Literal('0'), t.Literal('1')])
    ),
  }),
])
const app = new Elysia().post("/", (req) => req.body, {
  body: getBannersFilter
}).listen(3000);

console.log(
  `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);

// -----------------------------------------------------------------------
// Client Test
// -----------------------------------------------------------------------
async function request(body: typeof getBannersFilter.static) {
  const [endpoint, method, headers] = ['http://localhost:3000', 'post', {
    'Content-Type': 'application/json'
  }]
  const res = await fetch(endpoint, { method, headers, body: JSON.stringify(body)})
  return await res.json()
}

const A = await request({ query: '', take: 0, skip: 1 })
const B = await request({ query: '', take: 0, skip: 1, visible1: 1 })
const C = await request({ query: '', take: 0, skip: 1, visible2: '1' })
// const D = await request({ query: '', take: 0, skip: 1, visible2: '2' }) // expect error

console.log(A, B, C)

This outputs the following (which looks correct)

{
  query: "",
  take: 0,
  skip: 1
} {
  query: "",
  take: 0,
  skip: 1,
  visible1: 1
} {
  query: "",
  take: 0,
  skip: 1,
  visible2: "1"
}

You should be able to copy and paste the above and run your side. Again, I don't see any errors this side, but it might be worth trying to clear the Bun cache to ensure that previous versions of TypeBox are not installed. Other than that, you may want to try pinging the Elysia / Bun projects and referencing back to this issue if you're still having problems (as TypeBox seems to be working as expected here). Happy to provide assistance where I can, but would need to be able to repro the error first.

Hope this helps S

rhkdgns95 commented 1 year ago

@sinclairzx81 Thank you for the detailed reply!

If I add an error message in t.Union([t.Literal(0), t.Literal(1)]), will it work normally?

export const getBannersFilter = t.Composite([
  pagingFilterSchema,
  t.Object({
    // this is always failed
    visible1: t.Optional(
      t.Union([t.Literal(0), t.Literal(1)], { error: "this is not number type" })
    ),
  }),
]);
sinclairzx81 commented 1 year ago

@rhkdgns95 Hi, yes, the following seems to run fine.

Expand for Code ```typescript // ----------------------------------------------------------------------- // Server: bun create elysia app // ----------------------------------------------------------------------- import { Elysia, t } from "elysia"; export const pagingFilterSchema = t.Object({ query: t.String(), skip: t.Number(), take: t.Number() }) export const getBannersFilter = t.Composite([ pagingFilterSchema, t.Object({ visible1: t.Optional( t.Union([t.Literal(0), t.Literal(1)], { error: 'visible1 error' }) ), visible2: t.Optional( t.Union([t.Literal('0'), t.Literal('1')], { error: 'visible2 error' }) ), }), ]) const app = new Elysia().post("/", (req) => req.body, { body: getBannersFilter }).listen(3000); console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ); // ----------------------------------------------------------------------- // Test // ----------------------------------------------------------------------- async function request(body: typeof getBannersFilter.static) { const [endpoint, method, headers] = ['http://localhost:3000', 'post', { 'Content-Type': 'application/json' }] const res = await fetch(endpoint, { method, headers, body: JSON.stringify(body)}) return await res.json() } const A = await request({ query: '', take: 0, skip: 1 }) const B = await request({ query: '', take: 0, skip: 1, visible1: 1 }) const C = await request({ query: '', take: 0, skip: 1, visible2: '1' }) // const D = await request({ query: '', take: 0, skip: 1, visible2: '2' }) // expect error console.log(A, B, C) ```

The only error I see is for D which is passing an invalid value for visible2 (which is expected). Other than this, everything looks fine here.

rhkdgns95 commented 1 year ago

@sinclairzx81 Thank you for answer.

Interesting...

In the example, the request is being sent to the body.

In this case, I am also processed normally.

If you change it to GET Method,

Or is there a possibility that it will be recognized as a string when accessed by URL?

const app = new Elysia()
  .get("/", (req) => req.query, {
    query: getBannersFilter,
  })
  .listen(3000);

async function request(body: typeof getBannersFilter.static) {
  const [endpoint, method] = [
    "http://localhost:3000/?" + `query=example` + `&take=${0}` + `&skip=${1}` + `&visible=${1}`,
    "GET",
  ];
  const res = await fetch(endpoint, {
    method: "GET",
  });
  return await res.json();
}
sinclairzx81 commented 1 year ago

Or is there a possibility that it will be recognized as a string when accessed by URL?

Heya, this is a question best asked on the Elysia project (as TypeBox is only a runtime type system and doesn't process http requests (this is all handled by Elysia)). If you want to submit an issue to Elysia, it would be good to link back to this issue for reference.

rhkdgns95 commented 1 year ago

@sinclairzx81 all right.

However, t.Literal -> t.Numeric operates normally, so it is expected that it is not an http problem. Let's check a little more.

Thank you for your quick reply.

There's one more thing I'd like to ask.

Are we planning to support features like zod's "refine" or "superRefine" in the future?

sinclairzx81 commented 1 year ago

Let's check a little more.

All good :)

Are we planning to support features like zod's "refine" or "superRefine" in the future?

There are no immediate plans to implement Zod's refine or superRefine as validation constraints must always be serializable as standard Json Schema (making them compatible with validators that support this specification (so, Ajv and others)). There is room to explore implementing something similar under the TypeCompiler infrastructure in TypeBox, but isn't a high priority for the TypeBox project at this time.

Will close off this issue for now as the issue doesn't appear to be related to TypeBox. But feel free to link back to this issue for reference if you need.

All the best! S