sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.65k stars 150 forks source link

Interconnected interfaces, missing type annotations and maximum length exceeded. #824

Closed carrmelo closed 3 months ago

carrmelo commented 3 months ago

Hi, sorry to open an issue without a clear question, but I've been blocked for a while trying to solve the following structure without a good outcome. Maybe you could help point out the best approach to follow.

I have the below interfaces, but when trying to define them separately, some of their relationships break the inference of the type, resulting in any.

"Schema" implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022)

When trying to define only one big schema (just to get the result of it), I get the following error:

The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.ts(7056

I have reviewed many closed issues, looking for a guide to solve this, but it's been a bit challenging.

Is there any suggestion you could give me in order to continue my investigation and find a solution?

Do you see it feasible for me to define this relationship with Typebox?

After reading many of the issues I realized that there is a lot of Typescript documentations I should check too, so thanks in advance for any help you can provide

interface A {
    b?: B[];
    d?: D;
}

interface B {
    b?: B[];
    c?: C;
    d?: D;
}

export type C = {
    containers?: Record<string, A>;
};

interface B {
    b?: B[];
    d?: D;
}

interface D {
    items: E[];
}

type E = F | G | H;

interface F extends B {}
sinclairzx81 commented 3 months ago

@carrmelo Hiya,

TypeBox only has limited support for mutual recursive schematics. While you can express the Json Schema to capture mutual recursion, the type inference support is limited. The following is a best effort attempt to represent these types using TypeBox (using heavy use of Type.Ref to work around order dependency (or order of definitions)), but mileage may vary depending on the actual types you're trying to represent.

TypeScript Link Here

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

// interface A {
//   b?: B[];
//   d?: D;
// }
const A = Type.Object({
  b: Type.Optional(Type.Array(Type.Ref<typeof B>('B'))),
  d: Type.Optional(Type.Ref<typeof D>('D'))
}, { $id: 'A' })

// interface B {
//   b?: B[];
//   c?: C;
//   d?: D;
// }
const B = Type.Recursive(This => Type.Object({
  b: Type.Optional(Type.Array(This)),
  c: Type.Optional(Type.Ref<typeof C>('C')),
  d: Type.Optional(Type.Ref<typeof D>('D'))
}), { $id: 'B' })

// export type C = {
//   containers?: Record<string, A>;
// };
const C = Type.Object({
  containers: Type.Optional(Type.Record(Type.String(), Type.Ref('A')))
}, { $id: 'C' })

// interface D {
//     items: E[];
// }
const D = Type.Object({
  items: Type.Array(Type.Ref<typeof E>('E'))
}, { $id: 'D' })

// Missing

const G = Type.Object({}, { $id: 'G' })

// Missing

const H = Type.Object({}, { $id: 'H' })

// type E = F | G | H;

const E = Type.Union([Type.Ref('F'), G, H], { $id: 'E' })

// interface F extends B { }

const F = Type.Composite([B, Type.Object({})], { $id: 'F' })

Generally, if you can avoid mutually recursive types, that's going simplify things a lot. Also note that while the Json format can encode mutual recursive structures, you will need terminating types (like Null) that will ensure the mutually recursive structure can terminate which helps to avoid infinite loops when type checking (this is a bit of a deep topic)

// value.node.node.node.node.node.node.node.....infinity
const T = Type.Recursive(This => Type.Object({
  node: This
}))
const T = Type.Recursive(This => Type.Object({
  node: Type.Union([Type.Null(), This]) // note that the null union option means this type can terminate.
}))

So based on your original type example, there is a lot going on here. If it's helpful you can try use https://sinclairzx81.github.io/typebox-workbench to author your types. This will let you model types using TS syntax, with the generated TB type matching up. Just keep in mind that the workbench requires you to define types in order (so you have to define the type before using it in other types). It may be useful to reshape your types in a way that's friendly to TypeBox.

Hope this helps S

carrmelo commented 3 months ago

Thank you for all the input, S 🚀 . I have adjusted the definitions to your examples using Type.Ref, and even if I still get the error The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.ts(7056), at least I'm able to define it by types and not in just one block.

I will give it a go to typebox-workbench and continue the investigation over TS.

Thanks again! I'll close as this is more a question than an issue, and your answer is complete