gcanti / io-ts

Runtime type system for IO decoding/encoding
https://gcanti.github.io/io-ts/
MIT License
6.69k stars 328 forks source link

Is there a library to generate io-ts types from typescript code? #318

Open HoldYourWaffle opened 5 years ago

HoldYourWaffle commented 5 years ago

Is there a (working) library that can generate io-ts definitions from typescript code? For example:

Input:

type MyFantasticObject {
    amazingProperty: string;
    evenBetterProperty: number;
}

Output:

const MyFantasticObject = t.type({
    amazingProperty: t.string,
    evenBetterProperty: ts.number
})

I'm not looking for a magical 'on the go' solution, a simple CLI tool that generates an output file from an input file is more than enough. As far as I know this shouldn't be much harder than parsing the original input AST using the compiler api and printing new (transformed) AST based on that. I saw some discussion related to this in #243, but that question seems to be targeted to a magical 'on the go' solution, which I'm not really looking for.

Reason I'm asking this question (even though this is obviously out of scope for this project) is because I was going to write this, but I felt like this has to have been done before (no need to reinvent the wheel). I figured this would be the best place to ask.

The reason I want this is because regular typescript syntax is just way more natural & readable than the io-ts definitions (which is a natural side effect of the lack of good reflection tools).

gcanti commented 5 years ago

Is there a (working) library that can generate io-ts definitions from typescript code?

@HoldYourWaffle not that I'm aware of

As far as I know this shouldn't be much harder than parsing the original input AST using the compiler api

A couple of possible problems with this approach:

(1) how to handle imported types / global types?

import { AmazingProperty } from './other-file'

type MyFantasticObject = {
    amazingProperty: AmazingProperty; // <= imported type
    evenBetterProperty: Date; // <= global type
}

(2) how to handle refinements / branded types?

interface Person {
  name: string
  age: number // <= it would be nice to type this as a positive integer instead of a generic `number`
}
HoldYourWaffle commented 5 years ago

(1) how to handle imported types / global types?

For 'local project imports' I could just parse each file that's imported. I can't imagine this being much harder for npm modules. Globals might be a problem because there's no easy way to know where they're coming from (which is why globals are bad in most places). If the definition of the global type is passed into the program (either by import resolution or just having multiple input files) it could just parse (and generate) that definition as well. Apart from that I have no idea how it could work it out. I think the best solution in that case would be to just not support it.

(2) how to handle refinements / branded types?

Decorators maybe?

josejulio commented 5 years ago

@HoldYourWaffle Did you start something?

I would imagine that there would be limitations to what It can automatically do, but maybe not an issue to everyone.

I found this: https://github.com/gristlabs/ts-interface-builder It does what you ask but for other library.

andykais commented 5 years ago

Again, this does not generate io-ts types, it simply creates a validator for typescript types. typescript-is is a great little library. I use it in a few of my personal projects.

Because io-ts also does transformation on its inputs, I think that building a generator would require a fair amount of thought. It could work with a babel macro, or typescript transformer, but the jist is that some sort of interface needs to be handed to the developer to use in their source code. For example, how would the generated code describe casting a string to an int?

vegansk commented 5 years ago

Maybe offtopic, but I'm working on sbt plugin that can generate typescript types + io-ts definitions + scala types with xml/json codecs. Types definitions are written in scala dsl. It's not documented for now, but already used in production. If someone interested, I can speed up documentation creation :-)

DSL example:

  val Either = adt("Either")
    .generic("E".gen, "A".gen)
    .constructors(
      cons("Left").generic("E".gen, "A".gen).field("value", "E".gen, "Left value"),
      cons("Right").generic("E".gen, "A".gen).field("value", "A".gen, "Right value")
    )

Generated typescript code:

// ADT Either<E, A>

export type Either<E, A> = Either.Left<E, A> | Either.Right<E, A>

export namespace Either {
  export interface Left<E, A> {
    __tag: "Either.Left<E, A>"
    value: E /* Left value */
  }

  export interface Right<E, A> {
    __tag: "Either.Right<E, A>"
    value: A /* Right value */
  }
}

export namespace Either {
  export const Left = <E, A>(value: E): Either<E, A> => {
    return {
      __tag: "Either.Left<E, A>",
      value
    }
  }

  export const Right = <E, A>(value: A): Either<E, A> => {
    return {
      __tag: "Either.Right<E, A>",
      value
    }
  }
}

export namespace Either {
  export const LeftType: <E,A>(EType: t.Type<E>, AType: t.Type<A>) => t.Tagged<"__tag", Left<E, A>> = <E,A>(EType: t.Type<E>, AType: t.Type<A>) => typeImpl(
    {__tag: t.literal("Either.Left<E, A>"), value: EType}, {}, "Left"
  )

  export const RightType: <E,A>(EType: t.Type<E>, AType: t.Type<A>) => t.Tagged<"__tag", Right<E, A>> = <E,A>(EType: t.Type<E>, AType: t.Type<A>) => typeImpl(
    {__tag: t.literal("Either.Right<E, A>"), value: AType}, {}, "Right"
  )
}

export const EitherType: <E,A>(EType: t.Type<E>, AType: t.Type<A>) => t.Type<Either<E, A>> = <E,A>(EType: t.Type<E>, AType: t.Type<A>) => t.taggedUnion("__tag", [
  Either.LeftType(EType, AType), Either.RightType(EType, AType)
], "Either")
giogonzo commented 5 years ago

Since we are offtopic now: we also generate TS (io-ts) from Scala code, via metarpheus and metarpheus-io-ts. @vegansk to generate ADTs also have a look at fp-ts-codegen 😉

HoldYourWaffle commented 5 years ago

@josejulio I didn't, I ended up using TSOA for my auto-generated validation needs.

Lonli-Lokli commented 5 years ago

There is also this library https://github.com/teamdigitale/io-utils/blob/master/README.md

steida commented 5 years ago

Would be a nice addition to https://quicktype.io/

giogonzo commented 5 years ago

just came across to https://github.com/juusaw/ts-to-io

awerlogus commented 4 years ago

@HoldYourWaffle @gcanti @josejulio @andykais @vegansk @giogonzo @Lonli-Lokli @steida You're welcome to use my package https://github.com/awerlogus/io-ts-transformer

andykais commented 4 years ago

nice work @awerlogus. Recursive types really are the holy grail of this issue though. I dont believe anyone has been able to implement it yet.

awerlogus commented 4 years ago

@andykais @gcanti I just released a new version of my package. Now it can transform recursive types. You already can try it on yourself. https://github.com/awerlogus/io-ts-transformer

dgreene1 commented 4 years ago

tsoa is great for API validation. Here's an all purpose one typescript-to-validation library although I haven't tried it yet: https://github.com/gristlabs/ts-interface-checker

arnauorriols commented 3 years ago

Nice work @awerlogus , io-ts-transformer is exactly what I was looking for.

Out of curiosity: why TypeOf works io-ts => ts instead of the other way around?

In my opinion, from a design point of view having the de/coding lib as the source of truth is hard to sell.

samhh commented 3 years ago

It's not possible (without compiler plugins) to generate runtime code from TypeScript given type erasure / lack of runtime presence. The inverse however is possible with typeof as a sort of reflection.