ThomasAribart / json-schema-to-ts

Infer TS types from JSON schemas 📝
MIT License
1.47k stars 31 forks source link

When the schema is typed as JSONSchema, FromSchema<typeof schema> is never #45

Closed ghost closed 2 years ago

ghost commented 3 years ago

Hello,

The newly created type from a schema is never if the schema is typed as JSONSchema.

Let me demonstrate with an object example provided in the README.MD

When typed as JSONSchema

In the following example, I denote objectSchema variable as JSONSchema, and it is indeed helpful when I am typing properties. However, when I declare Object using objectSchema, its value is never.

import type { FromSchema, JSONSchema } from "json-schema-to-ts"

const objectSchema: JSONSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
} as const;
type Object = FromSchema<typeof objectSchema>;

Here you can see the type when I hover over Object: image


When NOT typed as JSONSchema

The very same example, however, works when I don't annotate objectSchema.

import type { FromSchema } from "json-schema-to-ts"

const objectSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
} as const;
type Object = FromSchema<typeof objectSchema>;

image

I'm really not sure why, and I'm confused as FromSchema is already defined as a generic expecting JSONSchema type, however, resulting in never when a value typed as JSONSchema.

export declare type FromSchema<S extends JSONSchema> = Resolve<ParseSchema<DeepWriteable<S>>>;
ThomasAribart commented 3 years ago

Hello @omer-to !

I understand it is confusing, but this is the expected behavior.

A constant (objectSchema here) can only have one type. If you don't assign one to it, TS will infer it from its definition. The as const statement forces it to be as narrow as possible. Notice the difference:

const objectSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
}

type testWidened = typeof objectSchema
// => { type: string, properties: { foo: string, bar: string }, required: string[] }
// The type is not narrow, for instance, the hard string "number" is widened as a general string

const objectSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
} as const

type testNarrow = typeof objectSchema
// => { type: "object" , properties: { foo: "string", bar: "number" }, required:["foo"] }
// (I omitted the readonly part for the sake of simplicity)
// The type is narrow, for instance "number" is kept as the hard string "number"

const objectSchema: JSONSchema = {
      type: "object",
      properties: {
            foo: { type: "string" },
            bar: { type: "number" },
      },
      required: ["foo"],
} as const;

type testAssigned = typeof objectSchema
// => JSONSchema
// The assigned type JSONSchema overrides the inferred type from TS

In order for FromSchema to be able to infer the valid type from a schema, the input schema type needs to be as narrow as possible. Otherwise, it won't be able to apply any logic to it. For instance, if any type keyword value is widened as a general string or as a union of hard strings ("object" | "array" | ...), FromSchema won't have enough information on the schema to operate. FromSchema<JSONSchema> will not return anything, the JSONSchema definition is too large to infer anything.

However, that doesn't prevent it from enforcing that the input schema type should follow a certain shape, and that means extending a certain type. That's the meaning of the S extends JSONSchema that you found in the definition. This allows S (which should be the narrow type { type: "object" , properties: { foo: "string", bar: "number" }, required:["foo"] }) to be validated against JSONSchema, while not being strictly equal to JSONSchema.

I hope it makes things a bit clearer. The main takeaway is that you cannot both assign JSONSchema and apply FromSchema to the same const simultaneously. Cheers !

Momics commented 2 years ago

@ThomasAribart I was struggling with a similar issue. I am trying to infer a JSONSchema argument of an execute function with this library. I was attempting it this way:

import { FromSchema, JSONSchema } from 'json-schema-to-ts';

type Method<Input = JSONSchema> = {
  input: Input;
  execute: (input: FromSchema<Input>) => void;
}

const method: Method = {
  input: {
    type: 'object',
    properties: {
      name: {
        type: 'string',
      }
    }
  },

  execute({ name }) {

  }
}

What I'd like to achieve is that the input JSON schema gets translated as a typed argument inside the execute function. As you explained above this is likely not possible this way... Do you have any ideas to achieve the same result? Or is this not possible?

Momics commented 2 years ago

Not sure why, but by using a function to create the method I do get proper type information:

import { JSONSchema, FromSchema } from "json-schema-to-ts";

function createMethod<Input extends JSONSchema>(options: {
  input: Input;
  execute: (input: FromSchema<Input>) => void;
}) {}

createMethod({
  input: {
    type: "object",
    properties: {
      name: {
        type: "string",
      },
    },
  } as const,
  execute: (input) => {
    return input.name; // string
  },
});
ThomasAribart commented 2 years ago

You can do the following:

import { FromSchema, JSONSchema } from 'json-schema-to-ts';

type Method<Input extends JSONSchema = JSONSchema> = {
  input: Input;
  execute: (input: FromSchema<Input>) => void; // <= Here error will not be raised because Input extends JSONSchema
}

const method: Method<typeof yourSchema> = {
  ...
}

The second method will work also 👍 I'm closing this issue :)

alphaone commented 1 year ago

@ThomasAribart Will the satisfies operator of Typescript 4.9 make it possible to check the type against JSONSchema?

ThomasAribart commented 1 year ago

Yup :) Gonna add it to the docs !

Zhincore commented 1 year ago

Yup :) Gonna add it to the docs !

I think it should be mentioned in the FAQ

https://github.com/ThomasAribart/json-schema-to-ts/blob/HEAD/documentation/FAQs/can-i-assign-jsonschema-to-my-schema-and-use-fromschema-at-the-same-time.md