Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type.Recursive does not work with Type.Transform for StaticDecode #895

Closed
joshuaavalon opened this issue Jun 11, 2024 · 3 comments
Closed

Comments

@joshuaavalon
Copy link

import { Type } from "@sinclair/typebox";

import type { StaticDecode, StaticEncode, TSchema } from "@sinclair/typebox";

const schema = Type.Recursive(self => Type.Object({
  a: Type.Number(),
  b: Type.Transform(Type.Union([self, Type.Null(), Type.Undefined()]))
    .Decode(v => v ?? undefined)
    .Encode(v => v)
}));

/**
type A = {
    a: number;
    b: ... | null | undefined;
}
 */
type A = StaticEncode<typeof schema>;

/**
type B = {
    a: number;
    b: undefined;
}
 */
type B = StaticDecode<typeof schema>;

You can see b is resolved to undefined even though it should be B | undefined.

@dearlordylord
Copy link

Hey, I worked on reproduction and my own report before finding there's a report already.

My issue seems to be the same. There's an additional issue reproduction I've prepared https://github.com/dearlordylord/typebox-recursion-transform-bug/blob/master/libs%2Ftypebox%2Fsrc%2Flib%2Ftypebox.ts

mport { Type, StaticDecode, Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

const Recursion = Type.Recursive(Self => Type.Object({
  id: Type.String(),
  children: Type.Transform(
    Type.Array(Self)
  ).Decode((v/*inferred as never[];*/) => v).Encode(v => v),
}));

type Recursion = StaticDecode<typeof Recursion>;

const r: {children: never[]} = Value.Decode(Recursion, {
  children: [
    { children: [] },
    { children: [{ children: [] }] },
  ],
});

function test(node: Recursion) {
  const a: never[] = node.children[0];
  // @ts-expect-error
  const b = node.children[0].children[0].id;
}

when it comes to .Transform inside .Recursive, the type seems to be erased to never

@sinclairzx81
Copy link
Owner

@joshuaavalon Hi, apologies for the delay.

So, the problem here is that the self parameter is unknown within the context of the Type.Recursive callback. Because the Transform holds onto static information about the encoded and decoded types, when it's passed a unresolved type, it can't reasonably infer from it, as the actual type is only derivable when the callbacks return value is known (which hasn't happened yet)

const R = Type.Recursive(self => Type.Object({
  x: Type.Transform(self)     // The self type is unresolved at this callsite. Because of this, the transform
    .Decode(value => value)   // holds onto the unresolved type, which infers as never on decode.
    .Encode(value => value), 
                     // ^ error here

})) // self is only known here

Unfortunately, there isn't an trivial fix for inferring transforms using unresolved recursive types. However you can invert the type by moving the transform to the outside.

import { Type, StaticDecode, StaticEncode } from '@sinclair/typebox'

const R = Type.Recursive(self => Type.Object({
  x: Type.Array(self)
}))

const T = Type.Transform(R) // self is evaluated in R
  .Decode(value => 1 as const)
  .Encode(value => ({ x: [{ x: [{ x: [] }] }] }))

type D = StaticDecode<typeof T> // type D: 1
type E = StaticEncode<typeof T> // type E: { x: ...[] }

Hope this lends some insight into the issue. I don't think this is going to be resolvable under the current design, so will close this one out as non-actionable. I would encourage you to try the second approach though and see if it meets your requirements. Happy to discuss more on this thread if you have any follow up questions.

Cheers
S

@joshuaavalon
Copy link
Author

@sinclairzx81 I know this problem may not be solvable when I tried to investigate.

Unfortunately, you workaround do not solve my problem because the example is a simplified case. The Transform is actually another type which is used as a properties of another type. So moving the Transform outside is not possible.

Anyway, thanks for your investigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants