io-ts icon indicating copy to clipboard operation
io-ts copied to clipboard

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

Open HoldYourWaffle opened this issue 5 years ago • 16 comments

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).

HoldYourWaffle avatar Apr 03 '19 20:04 HoldYourWaffle

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`
}

gcanti avatar Apr 04 '19 04:04 gcanti

(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?

HoldYourWaffle avatar Apr 04 '19 10:04 HoldYourWaffle

@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.

josejulio avatar Jul 29 '19 16:07 josejulio

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?

andykais avatar Jul 29 '19 17:07 andykais

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")

vegansk avatar Aug 06 '19 03:08 vegansk

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 😉

giogonzo avatar Aug 06 '19 07:08 giogonzo

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

HoldYourWaffle avatar Aug 21 '19 11:08 HoldYourWaffle

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

Lonli-Lokli avatar Sep 04 '19 20:09 Lonli-Lokli

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

steida avatar Sep 04 '19 21:09 steida

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

giogonzo avatar Sep 25 '19 08:09 giogonzo

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

awerlogus avatar Feb 21 '20 12:02 awerlogus

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.

andykais avatar Feb 25 '20 20:02 andykais

@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

awerlogus avatar Feb 27 '20 06:02 awerlogus

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

dgreene1 avatar Jul 30 '20 18:07 dgreene1

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.

arnauorriols avatar Feb 01 '21 16:02 arnauorriols

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.

samhh avatar Feb 01 '21 18:02 samhh