ThomasAribart / json-schema-to-ts

Infer TS types from JSON schemas 📝
MIT License
1.4k stars 30 forks source link

Simplest generic function doesn't work, because "too complex"? #165

Closed valyagolev closed 7 months ago

valyagolev commented 10 months ago
"typescript": "5.2.2"
"json-schema-to-ts": "^2.9.2"

I was trying to come up with a way to describe GPT functions, along with the name and description:

export type GPTFunction = {
  name: string;
  description?: string;
  parameters: { type: "object" } & JSONSchema;
};

export type Return<T extends GPTFunction> = FromSchema<T["parameters"]>;

export const SetImprovedDescription = {
  name: "set_improved_description",
  description: "Set improved description",
  parameters: {
    type: "object",
    properties: {
      description: { type: "string" },
      explanation_for_changes: { type: "string" },
    },
    required: ["description"],
    additionalProperties: false,
  },
} as const satisfies GPTFunction;

But then this doesn't work:

function call_gpt<T extends GPTFunction>(
  schema: T,
  system: string,
  user: string
): null | Return<T> {
  return null;
}

const a = call_gpt(SetImprovedDescription, "system", "user");

It errors out with:

Type instantiation is excessively deep and possibly infinite.ts(2589)
Expression produces a union type that is too complex to represent.ts(2590)

Most weirdly, while it does error out, it actually still correctly infers the type!

Screenshot 2023-09-02 at 17 18 58

I also tried simplifying the function declaration. Avoiding my types (Return and GPTFunction), and replacing them with their applications directly doesn't change the error.

This works, but it's not what I need unfortunately:

export function call_gpt2<T extends JSONSchema>(
  schema: T,
  system: string,
  user: string
): null | FromSchema<T> {
  return null;
}

const b = call_gpt2(SetImprovedDescription["parameters"], "system", "user");

Another attempt that doesn't work, and it's actually quite a simple thing:

function make_gpt_caller<T extends JSONSchema>(
  name: string,
  description: string,
  // parameters: Narrow<T> // tried both ways
  parameters: T
): (system: string, user: string) => FromSchema<T> {
  return (system: string, user: string) => {
    return null as any;
  };
}

No custom types, very simple generic... still I get, even without ever calling this, for "return":

Type instantiation is excessively deep and possibly infinite.ts(2589)
Expression produces a union type that is too complex to represent.ts(2590)

What do I miss? Did I misconfigure something, or is there a different ways to use generics here?

ThomasAribart commented 7 months ago

@valyagolev Hope I'm not answering too late.

I already had this issue in other libs. This is some TS internals dark magic that I don't understand anything about, but it will work if you use a second unconstrained generic like this:

export type GPTFunction = {
  name: string;
  description?: string;
  parameters: { type: "object" } & JSONSchema;
};

export type Return<T extends GPTFunction> = FromSchema<T["parameters"]>;

export const SetImprovedDescription = {
  name: "set_improved_description",
  description: "Set improved description",
  parameters: {
    type: "object",
    properties: {
      description: { type: "string" },
      explanation_for_changes: { type: "string" },
    },
    required: ["description"],
    additionalProperties: false,
  },
} as const satisfies GPTFunction;

// Will work !
function call_gpt<T extends GPTFunction, R = Return<T>>(
  schema: T,
  system: string,
  user: string,
): null | R {
  return null;
}

const a = call_gpt(SetImprovedDescription, "system", "user");

Will add a section in the FAQ about that!

ThomasAribart commented 7 months ago

FAQ added: https://github.com/ThomasAribart/json-schema-to-ts/blob/main/documentation/FAQs/applying-from-schema-on-generics.md

Cheers !