runtypes icon indicating copy to clipboard operation
runtypes copied to clipboard

Anyone generating runtype definitions by parsing TS interface/type declarations?

Open justingrant opened this issue 4 years ago • 7 comments

Has anyone built a tool that transforms TS interface declarations into Runtypes declarations? After using Runtypes for a while, I like the runtime API and the flexibility of adding additional logic (constraints, etc.). But I'm still unhappy with having to declare types using the Runtypes API instead of just writing my types using plain TypeScript which is (IMHO) easier to write, easier to read, won't strip JSDoc documentation, etc.

An ideal solution would be a build-time transformation (e.g. a Babel macro) that would emit a Runtype declaration that corresponds to a particular static type. Here's one possible way it could work:

// Like https://github.com/dsherret/ts-nameof/tree/master/packages/ts-nameof.macro,
// but for creating runtypes instead of simply extracting an identifier name
import runtypeof from 'ts-runtypeof.macro';

interface Foo { a: string, b: number }
function Bar (foo: Foo) {
  const FooRuntype = runtypeof<Foo>();
  FooRuntype.check(foo);
}

type Weird = /* put some types in here that can't be modeled in Runtypes */ ;
function BarWeird (weird: Weird) {
  const WeirdRuntype = runtypeof<Weird>(); // Babel should emit error & fail build
  WeirdRuntype.check(foo);
}

Obviously not all TS syntax would be supported, but that's true whether or not I'm manually writing the runtype definition code myself or something automatic is generating it.

@pelotom - This issue is essentially a dupe of #87. And I agree with your notes in that issue that building a solution is out of scope for this library. But the need is certainly there, and I think it'd be helpful to have a honeypot issue to attract people who might be building something similar (or who are interested in building it). So I was wondering if leaving this issue open for a year or two might be helpful. If you don't think it would be helpful, feel free to close.

Here's a list of related work happening elsewhere. Feel free to comment with others I didn't capture:

  • ts-rpc-http - generates validation based on TS types using codegen
  • typescript-is - generates type guard functions via a typescript transformer
  • ts-nameof - simple babel macro that extracts the name of an identifier into a constant that's accessible at runtime. Might be a useful example for building a similar runtypeof macro.
  • tsutils - wrapper functions to more easily use and manipulate TypeScript's AST
  • ts-morph - Another wrapper around TS's compiler API. Seems to be more popular than tsutils.
  • Developer docs for authoring Babel macros
  • tsguard.macro - proof-of-concept implementation of creating type guard functions from TS types. Not under active development.

One significant issue I've seen is the difference between Babel's vs. TSC's AST implementation. Most of the links above (maybe all?) use the TypeScript AST, which may have bad perf implications if used in an environment like create-react-app where Babel not TSC is used to transpile TypeScript files, because the TS compiler API would have to also run. But admittedly I'm not an expert on this stuff so I might be mistaken, but did want to capture it as a concern I've seen from others.

justingrant avatar Feb 13 '20 22:02 justingrant

Hey @justingrant, no objection to leaving this as an open issue for the sake of discussion. I share your desire to be able to just generate runtime validators from type definitions. Lots of good links you've provided here, thanks!

pelotom avatar Feb 15 '20 01:02 pelotom

That seems problematic, like const enums and namespaces with CRA. Please correct me if I'm wrong here, but if you separate transpiling from typechecking, won't any type-directed emit cause a problem? For example, if you generate runtypes definitions with babel, the values won't be available during typechecking and will cause a compile error. And if you did it with a transformer, then (with the standard tsc --noEmit or whatever in CRA) then that code wouldn't be transpiled by babel.

I guess these can be worked around, but all that seems fairly intrusive. I just realized that if you do it in babel you could generate a d.ts at the same time. But that still seems like a chicken-and-egg problem depending on which runs first (I thought babel ran first in CRA and it used the forking ts-loader?). I'm interested if I've gotten this all wrong.

I consistently point people towards runtypes when they ask for this kind of feature in the TS gitter/discord channels. The transform/macro routes just seem super painful in comparison to just writing runtypes, despite the kind of impedance mismatch. Also, runtypes definitions are more expressive (you can i.e. add additional constraints).

I'd honestly prefer codegen over a macro/transform. But that still wouldn't reach the goal of this issue, just writing types as usual.

keithlayne avatar Apr 13 '20 14:04 keithlayne

I created something like this: https://github.com/vedantroy/typecheck.macro, it just generates validation code directly, but I bet the codebase could be split into a few libraries & a new "runtypes" could be created.

vedantroy avatar Oct 20 '20 21:10 vedantroy

Hey @pelotom @justingrant @keithlayne @vedantroy,

After checking out your previous ideas, I've been going a different route, creating a CLI command to extract runtypes from TypeScript types. This has the benefits that this works cross-file, as one would expect, with the full TypeScript type inferrence.

You can find my code example here: https://gist.github.com/skurfuerst/a07ab23c3e40a45f2268f7700ceeceaf

Let me know what you think :-)

All the best, Sebastian

skurfuerst avatar Jan 16 '21 21:01 skurfuerst

Hello 👋

I recently published generate-runtypes. The purpose of this package is to be a building block for other libraries to use, like jsonschema-to-runtypes (which is just a fork of jsonschema-to-typescript, but converted to use generate-runtypes).

All of this is very experimental, but we been fortunate enough to get help from @runeh which has done amazing work to implement a better API to define Runtypes types. Next on my agenda is to try and build an OpenAPI to Runtypes+Client generator that can be used for many of my own API client packages I maintain.

I really wanted to get generate-runtypes into the wild, to get feedback and get people involved! Like @skurfuerst; I guess generate-runtypes is a great fit for your CLI command; Maybe you want to have a go at building a CLI command that converts Typescript types to Runtypes using generate-runtypes – it would be awesome!

If anyone of you want to chat and talk about the opportunities, please raise an issue on generate-runtypes or get in touch with me on keybase.

Thank you! 🙏

simenandre avatar Mar 25 '21 21:03 simenandre

It's worth mentioning https://github.com/akameco/s2s here. I've not yet tested it actually, but s2s (source-to-source) seems to be an interesting idea.

yuhr avatar Mar 31 '21 02:03 yuhr

I'm using @runtyping/runtypes for my project, and it's been very nice to use so far.

paskozdilar avatar Sep 04 '23 13:09 paskozdilar