sinclairzx81 / typebox-codegen

Code Generation for TypeBox Types
Other
91 stars 7 forks source link

TypeScriptToModel fails with type that contains a self reference #32

Open jeffrey-peterson-vanna opened 4 months ago

jeffrey-peterson-vanna commented 4 months ago

I'm working with TS files that contain references to types defined in the same interface. TypeScriptToModel seems to be using Recursive types for this scenario but fails when the nesting is more than one level deep.

Example:

import * as Codegen from '@sinclair/typebox-codegen';

const Code = `
  interface foo {
    bar: {
      ref: foo['bar']['baz'];
      baz: {};
    };
  }
`;

const boom = Codegen.TypeScriptToModel.Generate(Code);

TypeBox Workbench link

jpoehnelt commented 3 months ago

Running into this with Supabase generated types of the structure:

type Database = {
  public: {
    Tables: {
      foo: {
        Row: {
          bar: Database["public"]["Enums"]["Bar"];
        };
      };
    };
    Enums: {
      Bar: "baz" | "quz";
    };
  };
};
florence-wolfe commented 3 months ago

So I did a decent amount of investigation here and it's a fairly challenging problem to solve. I took a bunch of stabs at modifying the generated code to make lazily evaluate with getters or closures with no success.

I ended up trying to modify the typebox library directly instead. Only the Type.Index method relies on references, and self-references so this was a good place to focus. I made the Type.Index accept a callback function so that it could be evaluated lazily and then modify the generated code in typebox-codegen in my local forks. This worked and compiled successfully, however, the result of the output for the recursive/self-referential types resolves those references with an empty object.

This is still a great improvement over the previous code as it doesn't throw, but it's still not generating all of the final model's types correctly.

I think the next steps are to keep a reference tree in the generator of all self-references and iterate over them continuously until the desired reference is available, while avoiding circular references.

@sinclairzx81 Do you have any thoughts on this? I'm not even sure if this is the correct approach since this would require modifications to both the typebox and codegen libs. Seems wrong 😓

sinclairzx81 commented 3 months ago

@florence-wolfe Hi,

Unfortunately, there's no simple solution to this. With the exception of Type.Recursive, TypeBox self-referential types are generally not well supported with mapping types (like Index), and lazy types are generally considered to be out-of scope (as it primarily focuses on constructing immediate schematics).

The only way to really generate something like the proposed Database type would be to hoist the interior index type into it's own variable. The following layout should be supported with current library transforms, but the complexity to hoist the type may be a bit too prohibitively complex for this code generator.

Reference

import { Type } from '@sinclair/typebox'

// hoist the Bar out into it's own type
const ____Bar = Type.Union([
    Type.Literal('baz'),
    Type.Literal('bar')
])

const Database = Type.Object({
    public: Type.Object({
        Tables: Type.Object({
            foo: Type.Object({
                Row: Type.Object({
                    bar: ____Bar // js reference here
                })
            })
        }),
        Enums: Type.Object({
            Bar: ____Bar  // js reference here
        })
    })
})

There may be cases where the above may not work (specifically mutual recursive cases) but would be certainly be happy to review a PR if you wanted to try detect for self referential types. I would aim for the above layout as a possible starting point.

Hope this helps! S