joi-to-typescript icon indicating copy to clipboard operation
joi-to-typescript copied to clipboard

Extend interfaces when extending schemas

Open ThomasKuhlmann opened this issue 4 years ago • 11 comments

Hi!

I just realised that if you extend a schema, e.g.

const WorkspaceSchema = Joi.object({
 statistics:Joi.object({
     views: Joi.number()
     })
}).label('IWorkspace')

const ProjectSchema = WorkspaceSchema.keys({
  statistics:Joi.object({
    users:Joi.number()
    })
}).label('IProject')

You end up with two interfaces:

export interface IWorkspace {
  statistics: {
    views: number;
  };
}

and

export interface IProject {
  statistics: {
    users: number
   }
}

Wouldn't it make more sense to extend IProject with IWorkspace?

export interface IProject extends IWorkspace {
  statistics: {
    users: number
   }
}

ThomasKuhlmann avatar Jan 28 '21 10:01 ThomasKuhlmann

That would be nicer, I'm not sure if Joi exposes this type of information, but this can be investigated

mrjono1 avatar Jan 28 '21 11:01 mrjono1

I had a look around as well, but the documentation wasn't entirely conclusive... I've posted a question to the Joi repository, let's see what they come back with

https://github.com/sideway/joi/issues/2549

ThomasKuhlmann avatar Jan 28 '21 14:01 ThomasKuhlmann

@mrjono1 The team at Joi said the information isn't exposed by default. They had another suggestion to use .note() instead of .describe()?

https://github.com/sideway/joi/issues/2549#issuecomment-772010508

Not sure if that helps? I just realised that this isn't just a cosmetic issue, it seems part of the validation rules get lost when nested schemas get merged. If I extend this:

export const WorkspaceSchema = Joi.object({
  statistics: Joi.object({
    views: Joi.number().default(0),
    members: Joi.number().default(0),
  }).default(),
}).default()
  .label("IWorkspace")

with this:

export const PortfolioSchema = WorkspaceSchema.keys({
  statistics: Joi.object({
    risks: Joi.object({
      open: Joi.number().default(0),
      closed: Joi.number().default(0),
    }).default(),
  }).default(),
}).default()
  .label("IPortfolio")

you end up with these two interfaces:

IWorkspace

export interface IWorkspace {
  statistics: {
    members: number;
    views: number;
  };
}

IPortfolio

export interface IPortfolio {
  statistics: {
    risks: {
      closed: number;
      open: number;
    };
  };
}

But that's incorrect. Even if it doesn't end up extending ( interface IPortfolio extends IWorkspace), it should still merge the two statistics fields in the IPortfolio interface, shouldn't it?

export interface IPortfolio {
  statistics: {
    members: number;
    views: number;
    risks: {
      closed: number;
      open: number;
    };
  };
}

ThomasKuhlmann avatar Feb 05 '21 13:02 ThomasKuhlmann

It looks like they mean .label() vs .note(), as .describe() is what exports the Joi into a its own style of json schema. (I don't mean to be rude to the other team this isn't their library) Step 1 Supporting .label() and .note() to be the interface name sounds straight forward. There should only be one bit of code that determines the interface name eg if label does not exist use note

Step 2 Figuring out if you can do this, it does sound complicated but possible

One concern though is people may be using .note() the same way as .description() but I guess I could just note that as a FAQ item. I currently ignore note

mrjono1 avatar Feb 06 '21 02:02 mrjono1

I also found Joi.id() and Joi.extract() they seem interesting FYI I'm using .label() as it allows your joi to also work with https://github.com/glennjones/hapi-swagger

mrjono1 avatar Feb 07 '21 02:02 mrjono1

This would be a VERY helpful feature to have. If it is not able to be discovered via the Joi api, a solution using label or note seem very acceptable, if implemented in such a way that when specified it will add the extends NAME to the created interface.

Happy to try to help, not sure where to start in this codebase to create the functionality though.

kingmesal avatar May 06 '22 20:05 kingmesal

In the code where it gets the interface name, i think it will have an array of 'className's so you should be able to tell what to extend, but figuring out which properties are on the base and current will be hard

mrjono1 avatar May 06 '22 23:05 mrjono1

I have found in Joi where the data is, and modifying a test to be able to cover an implementation will be easy, because you already test concat. However, I cannot figure out where you write out the actual export interface Foo

    "metas": [
      {
        "className": "Key"
      },
      {
        "className": "Metadata"
      },
      {
        "className": "Value"
      },
      {
        "className": "CombinedRecord"
      }
    ],

it should just be able to write out export interface CombinedRecord extends Value, Metadata, Key

All the other logic is already in place for having properly created the interface. I'm just trudging through the code and have a rather difficult time interpreting where that work is being done.

kingmesal avatar May 08 '22 01:05 kingmesal

Can you point me to something that might help me better understand how to modify the code base to add the "extends" to the output template?

kingmesal avatar May 13 '22 20:05 kingmesal

yes the code has gotten a bit confusing, I'm not sure where we would need to add this concept, it will take time to figure out

mrjono1 avatar May 14 '22 07:05 mrjono1

I've made a start this adds the extends part https://github.com/mrjono1/joi-to-typescript/pull/235 next is to remove the fields that are being extended this may take a bit to tidy things up to make this do able

mrjono1 avatar May 19 '22 07:05 mrjono1