Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Demand for VSCode Extension? #96

Closed
bradennapier opened this issue Jul 25, 2020 · 11 comments
Closed

Demand for VSCode Extension? #96

bradennapier opened this issue Jul 25, 2020 · 11 comments
Labels
wontfix This will not be worked on

Comments

@bradennapier
Copy link

bradennapier commented Jul 25, 2020

Was playing around with VSCode and Zod based on #53 -- seeing if there is enough demand for this to put time into the concept. It is slow in the preview as I built that out in about 1 hour, but seems it could be nice - and make it easier to use Zod since you wouldn't really need to even learn all the specifics - you could just create your TypeScript Type then translate it.

It utilizes the TypeScript Compiler API to read the AST and infer from there.

Since this is just using the TypeScript compiler API - this could pretty easily become a browser tool as well which you could just paste in your type and get a valid zod schema out from it.

On that note, this is more of a Typescript Plugin than a VSCode Extension - since the vscode part of it is just providing the menu option to run the TS Compiler and it uses the TS Compiler to build Typescript Code.

export const generatePrimitive = ({ kind, name, props, zodImportValue }: {
  kind: ts.SyntaxKind;
  name: string;
  zodImportValue: string;
  props: { isOptional: boolean; errorMessage: undefined | string; isNullable?: boolean }
}) => {
  let flags = '';

  if (props.isOptional) {
    flags += '.optional()';
  }
  if (props.isNullable) {
    flags += '.nullable()';
  }

  let errorMessage = props.errorMessage ? wrapQuotes(props.errorMessage) : '';

  switch (kind) {
    case ts.SyntaxKind.NumericLiteral:
      return `${zodImportValue}.literal(${name})${flags}`;
    case ts.SyntaxKind.StringLiteral:
      return `${zodImportValue}.literal(${wrapQuotes(name)})${flags}`;
    case ts.SyntaxKind.StringKeyword:
      return `${zodImportValue}.string()${flags}`;
    case ts.SyntaxKind.BooleanKeyword:
      return `${zodImportValue}.boolean()${flags}`;
    case ts.SyntaxKind.NullKeyword:
      return `${zodImportValue}.null()${flags}`;
    case ts.SyntaxKind.UndefinedKeyword:
      return `${zodImportValue}.undefined()${flags}`;
    case ts.SyntaxKind.NumberKeyword:
      return `${zodImportValue}.number()${flags}`;
    case ts.SyntaxKind.AnyKeyword: 
      return `${zodImportValue}.any()${flags}`;
    case ts.SyntaxKind.BigIntKeyword:
      return `${zodImportValue}.bigint()${flags}`;
    case ts.SyntaxKind.VoidKeyword:
      return `${zodImportValue}.void()${flags}`;
    case ts.SyntaxKind.ClassKeyword: {
      if (name === 'Date') {
        return `${zodImportValue}.date()${flags}`;
      }
      // TODO : Handle Class & InstanceOf based on symbol & import detection context
    }
    default:
     return `${zodImportValue}.any(${errorMessage})`;
  }
};


The code above just uses strings to build it which was easier in this case. For those potentially interested, to move to using the TypeScript Compiler / AST to build it all, the generated type in the gif would be something like:

[
  ts.createVariableStatement(
    undefined,
    ts.createVariableDeclarationList(
      [ts.createVariableDeclaration(
        ts.createIdentifier("TestSchema"),
        undefined,
        ts.createCall(
          ts.createPropertyAccess(
            ts.createIdentifier("z"),
            ts.createIdentifier("object")
          ),
          undefined,
          [ts.createObjectLiteral(
            [
              ts.createPropertyAssignment(
                ts.createIdentifier("three"),
                ts.createCall(
                  ts.createPropertyAccess(
                    ts.createCall(
                      ts.createPropertyAccess(
                        ts.createIdentifier("z"),
                        ts.createIdentifier("literal")
                      ),
                      undefined,
                      [ts.createStringLiteral("hi")]
                    ),
                    ts.createIdentifier("optional")
                  ),
                  undefined,
                  []
                )
              ),
              ts.createPropertyAssignment(
                ts.createIdentifier("one"),
                ts.createCall(
                  ts.createPropertyAccess(
                    ts.createIdentifier("z"),
                    ts.createIdentifier("number")
                  ),
                  undefined,
                  []
                )
              ),
              ts.createPropertyAssignment(
                ts.createIdentifier("two"),
                ts.createCall(
                  ts.createPropertyAccess(
                    ts.createIdentifier("z"),
                    ts.createIdentifier("literal")
                  ),
                  undefined,
                  [ts.createNumericLiteral("3")]
                )
              ),
              ts.createPropertyAssignment(
                ts.createIdentifier("four"),
                ts.createCall(
                  ts.createPropertyAccess(
                    ts.createIdentifier("z"),
                    ts.createIdentifier("string")
                  ),
                  undefined,
                  []
                )
              ),
              ts.createPropertyAssignment(
                ts.createIdentifier("five"),
                ts.createCall(
                  ts.createPropertyAccess(
                    ts.createIdentifier("z"),
                    ts.createIdentifier("date")
                  ),
                  undefined,
                  []
                )
              )
            ],
            true
          )]
        )
      )],
      ts.NodeFlags.Const
    )
  ),
  ts.createTypeAliasDeclaration(
    undefined,
    undefined,
    ts.createIdentifier("Test"),
    undefined,
    ts.createTypeReferenceNode(
      ts.createQualifiedName(
        ts.createIdentifier("z"),
        ts.createIdentifier("infer")
      ),
      [ts.createTypeQueryNode(ts.createIdentifier("TestSchema"))]
    )
  )
];

-->

const TestSchema = z.object({
	three: z.literal('hi').optional(),
	one: z.number(),
	two: z.literal(3),
	four: z.string(),
	five: z.date()
})

type Test = z.infer<typeof TestSchema>;
@colinhacks
Copy link
Owner

Wow this is awesome!

Can't speak to the demand for this since all my current projects are "Zod native".

@danenania @Birowsky would this help you?

@bradennapier I'll leave this issue here and you can check back periodically to see how many 👀 it's getting.

@Birowsky
Copy link

@vriad damn right it would! I would love to have this feature on IntelliJ!

@bradennapier
Copy link
Author

bradennapier commented Aug 2, 2020

I added the code I built thus far which does most of the core properties and unions. It is a bit messy since I wasn't really focusing on building a polished release. Just wanted to see how hard it would be :-P

https://github.com/bradennapier/zod-vscode-extension

I think it'd be really cool if someone modified ts-ast-viewer to become an online tool that would allow you to paste in a type and get a zod schema back.

I tried to separate the vscode dependency as much as possible - all the code generation uses the ts compiler but there are some types that are sent into props that use VSCode's type - it would not be that hard to separate it completely though.

The process for figuring out how to use the compiler pretty much just involves copying what you want into the ts-ast-viewer and seeing what the ast will look like then copying and pasting it and/or figuring out how to query the node properly. I have queried almost every kind of type needed - just haven't finished building them out.

It should be pretty easy to support the rest. I added fns that make it pretty easy to make call fns and such

Supported:

  • - Literals
  • - Enums
  • - Objects
  • - Unions
  • - any
  • - Optional ( { val?: string })
  • - Nullable ( string | null ) --> string().nullable()
  • - Number/String modifiers (kind of supported via JSDoc tags - code is there but commented out atm)
  • - Unknown Keyword
  • - Date
  • - Void
  • - Null
  • - BigInt
  • - String
  • - Number
  • - Boolean
  • - Intersection - KIND OF - def needs more work since it would allow more than 2 values atm
  • - Tuples
  • - Recursive
  • - Promises
  • - Functions
  • - Arrays
  • - Records
  • - Unknown/Non Strict
  • - Type References (mostly)

Code is present for pretty much all the missing values in terms of parsing the AST, just need to generate the ts code and return it

@chrbala
Copy link
Contributor

chrbala commented Aug 2, 2020

Interesting concept! I would personally prefer to see GraphQL schema -> Zod types, but interesting either way! I suppose it's also possible to generate GraphQL -> TS -> Zod in some kind of pipeline as well. In general, I'd expect to auto-generate Zod types which then get manually edited. Certainly it's more complicated than just the proposal here, and I'm not sure exactly what it would look like in terms of UX.

@bradennapier
Copy link
Author

It'd be fairly easy to expand this to convert GraphQL to zod as well since GraphQL introspection just builds out an object you'd just need to translate it to the TS ast. I'd be surprised if there wasn't already tools for that as well.

@bradennapier
Copy link
Author

bradennapier commented Aug 2, 2020

Here is a little gif of the updated types that i added in the last hour. Also increased performance so it is about 90% faster. It is checking for the import name and using that or importing it as z by default - which is a config setting FYI (which is why it used ZOD here).

import * as ZOD from 'zod';

enum Value {
    one,
    two,
}

type Test = {
    one: string
}

export type MyTest = {
    one: 2 | 'hi' | Test;
    readonly two: any;
    three: Value
    four: {
        val?: string
    } & {
        another: number
    }
}

to

import * as ZOD from 'zod';

enum Value {
    one,
    two,
}

type Test = {
    one: string
}

export const MyTestSchema = ZOD.object({
    one: ZOD.union([
        ZOD.literal(2),
        ZOD.literal("hi"),
        ZOD.object({
            one: ZOD.string()
        })
    ]),
    two: ZOD.any(),
    three: ZOD.enum([
        "one",
        "two"
    ]),
    four: ZOD.intersection(ZOD.object({
        val: ZOD.string().optional()
    }), ZOD.object({
        another: ZOD.number()
    }))
});

export type MyTest = ZOD.infer<typeof MyTestSchema>;

@bradennapier
Copy link
Author

bradennapier commented Aug 3, 2020

https://github.com/bradennapier/zod-web-converter

Here it is running in web browser by just modifying the ts-ast-viewer source to include the conversion command. Could be a powerful tool to host where people could just open it and paste in their types and get the values they need out from that.

Could pretty easily also convert graphql as requested to be a pretty awesome all-encompassing tool.

You can build and play for yourself pretty easily just follow the start instructions. This is compiling a bunch of versions of typescript you can choose so it takes a bit to compile but that isn't really needed.

Screen Shot 2020-08-03 at 4 15 20 AM

Screen Shot 2020-08-03 at 4 16 54 AM

:-P

@danalexilewis
Copy link
Contributor

We were recently setting up a serverless function as a webhook end point to use with Typeform. As a starting place we ended up fleshing out a zod scheme to represent the incoming data. Now it didn't take long to write - but at the end I was wandering if this wouldn't be something that would be worth sharing in a similar way to definitely typed - a shared community repo of zod types. Because obviously they will need to be updated over time.

Then I saw this issue and thought "Damn my original idea was so naive" - it would simply be far better to have a way to zodify the definitions from any @type/* file you add to package.json

Maybe the function is a npx/cli/script that you run and commit the result in a zods folder? and it honours the versions defined in the yarn.lock/package-lock.json

Anyway @bradennapier thanks for the trail-blaze on this.

@bradennapier
Copy link
Author

bradennapier commented Oct 27, 2020

Yeah, a zodify would be nice, but that would be fairly difficult to do with any kind of confidence overall - just due to the massive difference of what Typescript can represent vs the small subset that Zod can (especially as new features like those being introduced in 4.1 come about).

That isn't to knock Zod, as there wouldn't really be much need to have it be a 1:1 mapping of what Typescript can represent (you really only need the basic data structuring).

This is why I liked the idea of the website to "zodify" as shown in the screenshot - essentially build out your data structure in typescript - paste it in and get you zod schema.

Perhaps I will be proven wrong though :-)

I have actually never used Zod... we use joi still on our project and define the types separately once validated... which is why I haven't done much more with it. I do hope we can look at using it in the future though at which point I am sure if others haven't taken up the idea, I would at least finish the remaining items listed.

@fabien0102
Copy link

@bradennapier I think I have a nice base for this kind of vscode extension, I tried to cover everything with unit tests & avoid any typecasting/any to be able to throw an explicit error when typescript can't be translated into a zod schema.
I also did find a nice trick for the "confidence" problem, I do generate an integration tests that compare z.infer and the original types (https://github.com/fabien0102/ts-to-zod/blob/main/example/heros.integration.ts).

I still need to think a bit how, but I guess we should be able to bake this integration test part inside the generation process 🤔

The project -> https://github.com/fabien0102/ts-to-zod

Since this issue seams to be quite popular, I will probably try to make a VSCode extension with this new library as core 😃 (I must admit that this is a really fun project 😁)

Thanks again for your little POC 💯

@stale
Copy link

stale bot commented Mar 2, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix This will not be worked on label Mar 2, 2022
@stale stale bot closed this as completed Mar 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

6 participants