ThomasAribart / json-schema-to-ts

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

Using a dictionary with `$ref` to reference predefined types #46

Closed kschaefe closed 2 years ago

kschaefe commented 3 years ago

First, your type mappings here are next level and as much as I've tried to follow along, I'll admit that I've gotten lost. That being said, I wonder if the following approach would work.

Your example of containing references creates a type that contains no reference to the original type:

const petSchema = {
  anyOf: [dogSchema, catSchema],
} as const;

The result is a unique type.

Consider the following example from the JSON Schema site (https://json-schema.org/understanding-json-schema/structuring.html):

{
  "$id": "https://example.com/schemas/address",

  "type": "object",
  "properties": {
    "street_address": { "type": "string" },
    "city": { "type": "string" },
    "state": { "type": "string" }
  },
  "required": ["street_address", "city", "state"]
}
{
  "$id": "https://example.com/schemas/customer",
  "type": "object",
  "properties": {
    "first_name": { "type": "string" },
    "last_name": { "type": "string" },
    "shipping_address": { "$ref": "/schemas/address" },
    "billing_address": { "$ref": "/schemas/address" }
  },
  "required": ["first_name", "last_name", "shipping_address", "billing_address"]
}

If we used a dictionary to map the $ref values, wouldn't the following be possible.

const addressSchema = { ... };
type Address = FromSchema<typeof AddressSchema>;

const customerSchema = { ... };
type customerSchemaDependencies = {
  "/schemas/address": Address
}
type Customer = FromSchemaWithDeps<typeof customerSchema, customerSchemaDependencies>;
// result type has reference to actual Address; instead of a copy

My little tests suggest that a dictionary approach for this would be possible:

type A1 = {};
type A2 = {};

type Deps = {
  foo: A1;
  bar: A2;
}

type Remapper<T, U> = {
  [P in keyof T]: T[P] extends keyof U ? U[T[P]] : T[P];
}

const Test = {
  a1: 'foo',
  a2: 'bar'
} as const

type Remapped = Remapper<typeof Test, Deps>;
/*
result:
type Remapped = {
    readonly a1: A1;
    readonly a2: A2;
}
*/

Is it possible to update FromSchema to use a dictionary in this way or would we need a new mapping type? Do you have an idea where that mapping could happen? I'd be willing to help, but as I mentioned before I found source rather complex.

ThomasAribart commented 2 years ago

Hi @kschaefe and thanks for the idea !

I had some thoughts about local definitions, that I think are definitely implementable without change in the FromSchema interface.

I hadn't thought about non-local definitions before, but yeah, it could totally exist. Something like:

type Customer = FromSchema<
  typeof customerSchema,
  { dependencies: {
      "/schemas/address": typeof schemaAddress
    }
  }
>

My priority is local definitions for now (which are already a challenge), but I'll look into it next 👍

milkcoke commented 2 years ago

We needs use local definitions like this. I'm using json-schema-to-ts with pg-tables-to-json since it's so tricky updating application code every time that changed database attribute name, type etc .. if not using ORM.

Scenario

1. Retrieve json schema from the database

I retrieve all table schema from my postgreSQL database using pg-tables-to-json. \ Result json file is like below

users.json

{
  "additionalProperties": false,
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "localhost:8080/public/users.json",
  "title": "users",
  "description": "User information table",
  "properties": {
    "id": {
      "type": "number",
      "description": " . Database type: integer. Default value: nextval('users_id_seq'::regclass)"
    },
  }
}

2. Define json schema in application code

user/schema.ts

import {FromSchema} from "json-schema-to-ts";
import {userSchema} from '@models/user/jsonSchema';
const registerUserParamJson = {
    type: 'object',
    properties: {
        id: {
            type: userSchema.properties.id.type,
            description: 'User ID'
        }
    },
    required: ['id']
} as const;

/* ⚠️ FIXME
    properties.id.type has primitive type 'string' and value is `number`
 */
// error occurs
type registerWalletAccountParam = FromSchema<typeof registerUserParamJson>;

Error message

Type 'string' is not assignable to type 'JSONSchema7TypeName | readonly JSONSchema7TypeName[] | undefined'.

This error occurred because not 'Literal type' even using as const retrieving type value dynamically

Can fix like this

// ...
      id: {
     // type assertion 
          type: userSchema.properties.id.type as 'number'
     }

Conclusion

We need supporting local schema definition gracefully.

ThomasAribart commented 2 years ago

Hi @milkcoke !

I don't think local schema definitions are the solutions here. In fact, in your case, I'm not sure json-schema-to-ts is the best solution, see this FAQ on why: https://github.com/ThomasAribart/json-schema-to-ts/blob/master/documentation/FAQs/does-json-schema-to-ts-work-on-json-file-schemas.md

I think using a code generator like https://www.npmjs.com/package/json-schema-to-typescript will suit your needs better (since you have to run a command anyway, might as well include the type generation in it).

ThomasAribart commented 2 years ago

Hi @kschaefe ! This feature is now available in v2.4.0 🥳

Check the references documentation to find examples !