Giter Club home page Giter Club logo

elm-ts-json's Introduction

elm-ts-json Build Status

Build up Encoders/Decoders with well-defined TypeScript types! The API in this package is the foundation of the elm-ts-interop CLI tool, but can be used for other purposes as well.

Core concepts

Figure out the types for your ports (to and from Elm)

How does elm-ts-interop figure out the types of your ports? There's no magic involved at all. You define a special type of Encoder/Decoder using the TsJson.Encoder and TsJson.Decoder modules.

These Decoders and Encoders that you write are the source of truth. It doesn't rely on implicitly generated Decoders or Encoders (Evan generally recommends against implicitly defined serialization because of limitations to that approach that he describes in his vision for data interchange). Instead, you define code with an API that is very similar to elm/json. But this API knows the type information of the Encoder or Decoder you've built.

For example, if you use TsDecode.string, it knows that will expect to receive a string from TypeScript. TsDecode.list TsDecode.string will expect to receive string[] from TypeScript. You can even build up more expressive TypeScript types like Unions:

import TsJson.Decode as TsDecode
import TsJson.Type

TsDecode.oneOf
    [ TsDecode.string
    , TsDecode.int |> TsDecode.map String.fromInt
    ]
    |> TsDecode.tsType
    |> TsJson.Type.toTypeScript
    --> "string | number"

So we've written a Decoder very similar to how we would with elm/json, but this Decoder has one key difference. We can turn the Decoder itself into a TypeScript type (string | number). And without even passing a value through the Decoder From this simple idea, you can build very sophisticated typed interop, even sending/receiving Elm Custom Types, and sending/receiving TypeScript discriminated unions.

Example

import Json.Encode as JE
import TsJson.Encode as Encode exposing (Encoder)
import TsJson.Decode as Decode exposing (Decoder)

type User
    = Regular { name : String }
    | Guest

userEncoder : Encoder User
userEncoder =
    Encode.union
        (\vRegular vGuest value ->
            case value of
                Regular data ->
                    vRegular data
                Guest ->
                    vGuest
        )
        |> Encode.variant
            (Encode.object
                [ Encode.required "kind" identity (Encode.literal <| JE.string "regular")
                , Encode.required "name" .name Encode.string
                ]
            )
        |> Encode.variantLiteral (JE.object [ ( "kind", JE.string "guest" ) ])
        |> Encode.buildUnion


userDecoder : Decoder User
userDecoder =
    Decode.oneOf
        [ Decode.succeed (\() name -> Regular { name = name })
            |> Decode.andMap (Decode.field "kind" (Decode.literal () (JE.string "regular")))
            |> Decode.andMap (Decode.field "name" Decode.string)
        , Decode.literal Guest (JE.object [ ( "kind", JE.string "guest" ) ])
        ]

type alias Name =
    { first : String, last : String }


nameEncoder : Encoder Name
nameEncoder =
    Encode.object
        [ Encode.required "first" .first Encode.string
        , Encode.required "last" .last Encode.string
        ]


nameDecoder : Decoder Name
nameDecoder =
    Decode.succeed Name
        |> Decode.andMap (Decode.field "first" Decode.string)
        |> Decode.andMap (Decode.field "last" Decode.string)

Guest
    |> Encode.runExample userEncoder
--> { output = """{"kind":"guest"}"""
--> , tsType = """{"kind":"guest"} | { kind : "regular"; name : string }"""
--> }

userDecoder |> Decode.runExample """{"kind":"guest"}"""
--> { decoded = Ok Guest
--> , tsType = """{ kind : "regular"; name : string } | {"kind":"guest"}"""
--> }

elm-ts-json's People

Contributors

dillonkearns avatar hansallis avatar jfmengels avatar pabra avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

elm-ts-json's Issues

missing quotes

If you have a custom type and Codec like this:

type MyType
    = MyType Int


codec : Codec MyType
codec =
    Codec.custom Nothing
        (\vMyType value ->
            case value of
                MyType int ->
                    vMyType int
        )
        |> Codec.namedVariant1 "MyType" MyType ( "needs-quotes", Codec.int )
        |> Codec.buildCustom

it will generate this TypeScript type (missing quotes around missing-quotes):

{ needs-quotes : number; tag : "MyType" }

here is an ellie

BTW: thanks for this project ๐Ÿ˜

Use opaque type for andThen, allow succeed and fail

@neurodynamic thanks again for the feedback on the API.

To follow up on the Discourse discussion, I think it would make sense to use an opaque type like the UnionEncodeValue type.

The benefit is that the library then guarantees that all possible decoders that may continue on from the andThen have been "registered". Otherwise, you could use any decoder within the andThen as it is in its current state, and that will cause the decoder to have inaccurate type information.

Here's an example with the current API:

                field "version" int
                    |> andThen
                        (andThenInit
                            (\version ->
                                case version of
                                    1 ->
                                        field "firstName" string

                                    _ ->
                                        at [ "name", "first" ] string
                            )
                        )
                    |> expectDecodes
                        { input = """{"version": 2, "name": {"first": "Jane"}}"""
                        , output = "Jane"
                        , typeDef = "{ version : number }"

Note that the type def doesn't have any information about the firstName or name.first fields. This is inaccurate.

If you "register" the continuation decoders, as is intended, then you get nice, accurate TypeScript type information:

field "version" int
    |> andThen
        (andThenInit
            (\v1Decoder v2Decoder version ->
                case version of
                    1 ->
                        v1Decoder

                    _ ->
                        v2Decoder
            )
            |> andThenDecoder (field "firstName" string)
            |> andThenDecoder (at [ "name", "first" ] string)
        )
    |> expectDecodes
        { input = """{"version": 2, "name": {"first": "Jane"}}"""
        , output = "Jane"
        , typeDef = "({ version : number } & { name : { first : string } } | { firstName : string })"
        }

I think this is quite nice! The improvement here would be for the API to enforce using the API in this intended way.

Guaranteeing correct usage with opaque types

That's where the opaque type comes in. Right now, the continuation takes a TsJson.Decode.Decoder. If we require it to use a simple wrapper type around that type (let's call it AndThenDecoder), then we can guarantee that the only way to get that wrapped type is by passing it through andThenDecoder ....

type AndThenDecoder a = AndThenDecoder (Decoder a)

succeed and fail

I think these two types may be special cases. You can't "register" either of these with andThenDecoder, because that would force you to have the success value or failure message when you register it. But that defeats the purpose of andThen.

I think adding these functions to the API will solve that problem:

andThenSucceed : a -> AndThenDecoder a
andThenSucceed a =
    AndThenDecoder (succeed a)

andThenFail : String -> AndThenDecoder a
andThenFail errorMessage =
    AndThenDecoder (fail errorMessage)

As far as the resulting TypeScript type defs from a Decoder, using succeed or fail within andThen doesn't change that. So I think this design will work nicely!

Possible to define elm type and encoder & decoder from typescript?

Currently this library allow one to define typescript type and elm type and encoder & decoder from combinator (source code) written in elm.

Is there another way. That allow one to define elm type and encoder & decoder from typescript type (parse from source code) or combinator functions / ast written in typescript ?

'Elm' refers to a UMD global typescript error in generated file

The generated typescript file contains the following lines:

export as namespace Elm;

export { Elm };

which raises the following error when I try to build:

gen/mainBack.d.ts:36:10 - error TS2686: 'Elm' refers to a UMD global, but the current file is a module. Consider adding an import instead.

36 export { Elm };
            ~~~

removing this export {Elm } line makes Typescript happy.... Is it necessary to add this export { Elm } in the generated file? If yes, how could I solve this error and be able to build without performing a ugly sed?

Add support for int unions

Currently I use

TsJson.Codec.stringUnion
        [ ( "0", Reliable )
        , ( "1", Lossy )
        , ( "-1", Unrecognized )
        ]

to encode data for a JS library. The resulting type is "0" | "1" | "-1" and I have to call parseInt on the JS side to prepare data for the library. It would be nice to simply write

TsJson.Codec.intUnion
        [ ( 0, Reliable )
        , ( 1, Lossy )
        , ( -1, Unrecognized )
        ]

to get 0 | 1 | -1 type and avoid extra conversion on the JS side.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.