ThomasAribart / json-schema-to-ts

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

Types not resolving correctly after 2.9.2 #183

Open hamanuha opened 6 months ago

hamanuha commented 6 months ago

Hi,

first of all I want to say thank you for this great library!

I use it to share typed api routes between backend and frontend. Basically I define a contract using json schemas. This is then used for the validation and for the types on the backend and for creating the queries for the frontend.

Unfortunately upgrading to 2.12.0 or 3.0.0 breaks my code. I heavily simplified the example below to show the issue. Some types seem to be not needed but this is due to the simplification.

If I define the schema externally for initContract then this works with the newer versions as well. But my inline schema for the params property only works with version 2.9.2.

Any idea what causes this behaviour? I would like to continue using inline schema definitions.

Types with 2.9.2

types-2-9-2

Types with 2.12.0

types-2-12-0

Simplified example

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

// Simplified version
type ContractRoute = {
  params: JSONSchema
  query: JSONSchema
}

type Contract = {
  [key: string]: ContractRoute
}

type ProcessContract<T extends Contract> = {
  [K in keyof T]: T[K]
}

const initContract = <TRouter extends Contract>(routes: ProcessContract<TRouter>): TRouter => routes

type ContractRouteImplementation<T extends ContractRoute> = () => {
  handler: (request: { Params: FromSchema<T['params']>; Query: FromSchema<T['query']> }) => void
}

type ServerObj<T extends Contract> = {
  [TKey in keyof T]: ContractRouteImplementation<T[TKey]>
}

type InitialisedServer<T extends Contract> = {
  contract: T
  routes: ServerObj<T>
}

const initServer = <T extends Contract>(contract: T, routes: ServerObj<T>): InitialisedServer<T> => ({
  contract,
  routes,
})

// Example
const externSchema = {
  type: 'object',
  properties: {
    str: { type: 'string' },
  },
  required: ['str'],
  additionalProperties: false,
} as const

const contract = initContract({
  demo: {
    params: {
      type: 'object',
      properties: {
        str: { type: 'string' },
      },
      required: ['str'],
      additionalProperties: false,
    } as const,
    query: externSchema,
  },
})

initServer(contract, {
  demo: () => ({
    handler: async (request) => {},
  }),
})
ThomasAribart commented 6 months ago

Hi @hamanuha and thanks for reaching out!

This seems to be related to https://github.com/ThomasAribart/json-schema-to-ts/issues/165, you can look at https://github.com/ThomasAribart/json-schema-to-ts/blob/main/documentation/FAQs/applying-from-schema-on-generics.md

This worked for me:

type ContractRouteImplementation<
  CONTRACT_ROUTE extends ContractRoute,
  PARAMS = FromSchema<CONTRACT_ROUTE["params"]>,
  QUERY = FromSchema<CONTRACT_ROUTE["query"]>,
> = () => {
  handler: (request: { Params: PARAMS; Query: QUERY }) => void;
};
hamanuha commented 6 months ago

Hi @ThomasAribart,

thanks for the hint. Unfortunately this does not work for me. I am using typescript 5.3.3. Maybe it's related to some config values?

Would you mind sharing your tsconfig?

Edit: When using your asConst function it works with both versions of the ContractRouteImplementation. But only if I use as const as well.

const contract = initContract({
  demo: {
    params: asConst({
      type: 'object',
      properties: {
        str: { type: 'string' },
      },
      required: ['str'],
      additionalProperties: false,
    } as const),
    query: externSchema,
  },
})

Edit2: When using the Narrow type from your package inside FromSchema it works also for the inline schema without having to use asConst

type ParamsFromSchema<T extends ContractRoute, TParams = Narrow<T['params']>> = TParams extends JSONSchema
  ? FromSchema<TParams>
  : never
hamanuha commented 4 months ago

Hey @ThomasAribart,

did you see my second edit? Can I help with something to resolve this issue?