vedantroy / typecheck.macro

A easy (and fast) typescript validation library/macro. Automatically generate validation functions for Typescript types.
MIT License
413 stars 7 forks source link

Support "extends" and intersection operator. #2

Open vedantroy opened 4 years ago

vedantroy commented 4 years ago

Support

type A = B & C;

and

interface Foo extends Bar {}
lukasluecke commented 4 years ago

This (and basic mapped types, like Record) is the main issue that prevents me from using this to it's full potential in a project that uses intersection types a lot (as I have to duplicate / manually "intersect" the types to validate right now, which kinda defeats the purpose.. 😅)

Is there anything specific you're looking for help with?

vedantroy commented 4 years ago

I'm just finished a big refactor that will allow me to implement type intersection + other features.

Quick question: With the types you are intersecting, are they both in the same file?

lukasluecke commented 4 years ago

I saw your commits, looking forward to the next version 🙂 For now I've restructured the parts that I typecheck to all be in the same file and without any intersections or mapped types, but of course it would be a lot nicer to be as flexible as possible with the locations and separations 😉

vedantroy commented 4 years ago

So the reason I asked about the types being in different files was because I just realized I can't validate multi-file types (Foo imports Bar from another file) using just a macro.

As such, I am trying to figure out if this is a big issue for users. If it is, I will build a CLI tool that can handle multi-file types.

lukasluecke commented 4 years ago

Mh.. not sure how big of a problem that would be. Would registering the imported type / creating a separate validator in the source file help at all? (is there a way to "connect" these afterwards?)

I think it would probably be fine in most cases for now, seeing as in most cases you could just move the types to where the validation is created, and then import it in the other files from there instead. What about "library" types (something like Record), would these also be affected the same way?

vedantroy commented 4 years ago

The fundamental issue is that Babel processes files one at a time. So I cannot wait until all files have been processed to build a global "namespace" of types.

The original idea was as you are describing: "register" the imported type in the source file. However, this doesn't work reliably because if the file with the exported type/the register call is processed after the file that consumes the registered/exported type, the macro will throw an error.

Library types won't be affected because I can just hardcode library types like Set, Map, Record into the macro. (Infact, this is what I already do).

Another thing you can do is, instead of moving your types to where you want to generate your validation functions, just generate your validation functions where you declare your types and export them. This works because validation functions are just normal Javascript that can be imported/exported like anything else.

schontz commented 4 years ago

What is the status of this issue? Assuming I have all my types in the same file...

vedantroy commented 4 years ago

@schontz Intersection is supported. extends is not supported, but I don't think it would be too bad to support it.

Let me know if "extends" support is important!

schontz commented 4 years ago

For my uses, yes, I use extends all over the place. Is any form of extends supported?

vedantroy commented 4 years ago

Hmm, right now no. I'll take a shot at implementing this -- it seems like the extends algorithm is simpler than intersection anyway.

In the meantime, intersection is a pretty viable alternative. It covers pretty much everything extends does I think.

schontz commented 4 years ago

So just convert

interface Foo extends Bar {
  id: string;
}

to:

type Foo = Bar & {
  id: string;
}

Like that?

vedantroy commented 4 years ago

Yes, that should work.

schontz commented 4 years ago

OK. But what if I have generics? For example:

interface Foo<E extends string> {
  data: E;
}

interface Bar extends Foo<'bar'> {}

const bar: Bar = { data: 'foo' }; // error
const bar2: Bar = { data: 'bar' }; // good
vedantroy commented 4 years ago

I believe intersection can cover everything extends does.

The above example would become:

interface Foo<E extends string> {
    data : E
}

type Bar = Foo<'bar'>

There's no intersection involved because the body of the Bar interface is empty.

If the body of the interface wasn't empty (let's say it was: data2: Q, where Q is another generic parameter) then you would do

type Bar<Q> = Foo<'bar'> & { data2: Q }
schontz commented 4 years ago

Ok great. I'll see if I can get this to work. Thanks for all your help.

E extends string doesn't cause any issues?

vedantroy commented 4 years ago

Hm, I should check that it doesn't. If it does -- I can fix that pretty quickly, since upper bound at types are compile time constraints that are not relevant at runtime.

schontz commented 4 years ago

OK. No big deal, because I control enough of my code that I can drop E extends string. If I make E a number, shame on me!

vedantroy commented 4 years ago

Just tested -- the E extends string inside of the type parameter is supported.

schontz commented 4 years ago

This is great. Looking forward to playing with this.