dillonkearns / elm-ts-json / TsJson.Encode

The TsJson.Encode module is what you use for

See TsJson.Decode for the API used for Flags and ToElm Ports.

By building an Encoder with this API, you're also describing the source of truth for taking an Elm type and turning it into a JSON value with a TypeScript type. Note that there is no magic involved in this process. The elm-ts-interop CLI simply gets the typeDef from your Encoder to generate the TypeScript Declaration file for your compiled Elm code.


type alias Encoder input =
TsJson.Internal.Encode.Encoder input

Similar to a Json.Encode.Value in elm/json. However, a TsJson.Encode.Encoder in elm-ts-json has this key difference from an elm/json Encode.Value:

So the elm-ts-json Encoder expects a specific type of Elm value, and knows how to turn that Elm value into JSON.

Let's compare the two with an example for encoding a first and last name.

import Json.Encode

elmJsonNameEncoder : { first : String, last : String }
    -> Json.Encode.Value
elmJsonNameEncoder { first, last } =
    Json.Encode.object
        [ ( "first", Json.Encode.string first )
        , ( "last", Json.Encode.string last )
        ]

{ first = "James", last = "Kirk" }
        |> elmJsonNameEncoder
        |> Json.Encode.encode 0
--> """{"first":"James","last":"Kirk"}"""

nameEncoder : Encoder { first : String, last : String }
nameEncoder =
    object
        [ required "first" .first string
        , required "last" .last string
        ]

{ first = "James", last = "Kirk" }
        |> runExample nameEncoder
--> { output = """{"first":"James","last":"Kirk"}"""
--> , tsType = "{ first : string; last : string }"
--> }

Built-Ins

string : Encoder String

Encode a string.

import Json.Encode as Encode

"Hello!"
    |> runExample string
--> { output = "\"Hello!\""
--> , tsType = "string"
--> }

You can use map to apply an accessor function for how to get that String.

{ data = { first = "James", last = "Kirk" } }
    |> runExample ( string |> map .first |> map .data )
--> { output = "\"James\""
--> , tsType = "string"
--> }

int : Encoder Basics.Int

import Json.Encode as Encode

123
    |> runExample int
--> { output = "123"
--> , tsType = "number"
--> }

float : Encoder Basics.Float

import Json.Encode as Encode

123.45
    |> runExample float
--> { output = "123.45"
--> , tsType = "number"
--> }

literal : Json.Encode.Value -> Encoder a

TypeScript has the concept of a Literal Type. A Literal Type is just a JSON value. But unlike other types, it is constrained to a specific literal.

For example, 200 is a Literal Value (not just any number). Elm doesn't have the concept of Literal Values that the compiler checks. But you can map Elm Custom Types nicely into TypeScript Literal Types. For example, you could represent HTTP Status Codes in TypeScript with a Union of Literal Types like this:

type HttpStatus = 200 | 404 // you can include more status codes

The type HttpStatus is limited to that set of numbers. In Elm, you might represent that discrete set of values with a Custom Type, like so:

type HttpStatus
    = Success
    | NotFound

However you name them, you can map those Elm types into equivalent TypeScript values using a union of literals like so:

import Json.Encode as Encode

httpStatusEncoder : Encoder HttpStatus
httpStatusEncoder =
    union
        (\vSuccess vNotFound value ->
            case value of
                Success ->
                    vSuccess
                NotFound ->
                    vNotFound
        )
        |> variantLiteral (Encode.int 200)
        |> variantLiteral (Encode.int 404)
        |> buildUnion

NotFound
    |> runExample httpStatusEncoder
--> { output = "404"
--> , tsType = "404 | 200"
--> }

bool : Encoder Basics.Bool

import Json.Encode as Encode

True
    |> runExample bool
--> { output = "true"
--> , tsType = "boolean"
--> }

null : Encoder input

Equivalent to literal Encode.null.

import Json.Encode as Encode

()
    |> runExample null
--> { output = "null"
--> , tsType = "null"
--> }

Transforming

map : (input -> mappedInput) -> Encoder mappedInput -> Encoder input

An Encoder represents turning an Elm input value into a JSON value that has a TypeScript type information.

This map function allows you to transform the Elm input value, not the resulting JSON output. So this will feel different than using TsJson.Decode.map, or other familiar map functions that transform an Elm output value, such as Maybe.map and Json.Decode.map.

Think of TsJson.Encode.map as changing how to get the value that you want to turn into JSON. For example, if we're passing in some nested data and need to get a field

import Json.Encode as Encode

picardData : { data : { first : String, last : String, rank : String } }
picardData = { data = { first = "Jean Luc", last = "Picard", rank = "Captain" } }

rankEncoder : Encoder { data : { officer | rank : String } }
rankEncoder =
    string
        |> map .rank
        |> map .data

picardData
    |> runExample rankEncoder
--> { output = "\"Captain\""
--> , tsType = "string"
--> }

Let's consider how the types change as we map the Encoder.

encoder1 : Encoder String
encoder1 =
    string

encoder2 : Encoder { rank : String }
encoder2 =
    string
        |> map .rank

encoder3 : Encoder { data : { rank : String } }
encoder3 =
    string
        |> map .rank
        |> map .data

(encoder1, encoder2, encoder3) |> always ()
--> ()

So map is applying a function that tells the Encoder how to get the data it needs.

If we want to send a string through a port, then we start with a string Encoder. Then we map it to turn our input data into a String (because string is Encoder String).

encoderThing : Encoder { data : { officer | first : String, last : String } }
encoderThing =
    string
        |> map (\outerRecord -> outerRecord.data.first ++ " " ++ outerRecord.data.last)

picardData
    |> runExample encoderThing
--> { output = "\"Jean Luc Picard\""
--> , tsType = "string"
--> }

Objects

object : List (Property input) -> Encoder input

import Json.Encode as Encode

nameEncoder : Encoder { first : String, last : String }
nameEncoder =
    object
        [ required "first" .first string
        , required "last" .last string
        ]


{ first = "James", last = "Kirk" }
        |> runExample nameEncoder
--> { output = """{"first":"James","last":"Kirk"}"""
--> , tsType = "{ first : string; last : string }"
--> }

fullNameEncoder : Encoder { first : String, middle : Maybe String, last : String }
fullNameEncoder =
    object
        [ required "first" .first string
        , optional "middle" .middle string
        , required "last" .last string
        ]

{ first = "James", middle = Just "Tiberius", last = "Kirk" }
        |> runExample fullNameEncoder
--> { output = """{"first":"James","middle":"Tiberius","last":"Kirk"}"""
--> , tsType = "{ first : string; last : string; middle? : string }"
--> }


type Property input

optional : String -> (input -> Maybe mappedInput) -> Encoder mappedInput -> Property input

required : String -> (input -> mappedInput) -> Encoder mappedInput -> Property input

Union Types

import Json.Encode as Encode

type ToJs
    = SendPresenceHeartbeat
    | Alert String

unionEncoder : Encoder ToJs
unionEncoder =
    union
        (\vSendHeartbeat vAlert value ->
            case value of
                SendPresenceHeartbeat ->
                    vSendHeartbeat
                Alert string ->
                    vAlert string
        )
        |> variant0 "SendPresenceHeartbeat"
        |> variantObject "Alert" [ required "message" identity string ]
        |> buildUnion


Alert "Hello TypeScript!"
        |> runExample unionEncoder
--> { output = """{"tag":"Alert","message":"Hello TypeScript!"}"""
--> , tsType = """{ tag : "Alert"; message : string } | { tag : "SendPresenceHeartbeat" }"""
--> }


type alias UnionBuilder match =
TsJson.Internal.Encode.UnionBuilder match

union : constructor -> UnionBuilder constructor

variant : Encoder input -> UnionBuilder ((input -> UnionEncodeValue) -> match) -> UnionBuilder match

variant0 : String -> UnionBuilder (UnionEncodeValue -> match) -> UnionBuilder match

variantObject : String -> List (Property arg1) -> UnionBuilder ((arg1 -> UnionEncodeValue) -> match) -> UnionBuilder match

variantLiteral : Json.Encode.Value -> UnionBuilder (UnionEncodeValue -> match) -> UnionBuilder match

variantTagged : String -> Encoder input -> UnionBuilder ((input -> UnionEncodeValue) -> match) -> UnionBuilder match

Takes any Encoder and includes that data under an Object property "data".

For example, here's an encoded payload for a log event.

import TsJson.Encode as TsEncode
import Json.Encode

type alias Event = { level : String, message : String }
type FromElm = LogEvent Event

eventEncoder : Encoder Event
eventEncoder =
    TsEncode.object
        [ TsEncode.required "level" .level TsEncode.string
        , TsEncode.required "message" .message TsEncode.string
        ]


fromElm : TsEncode.Encoder FromElm
fromElm =
    TsEncode.union
        (\vLogEvent value ->
            case value of
                LogEvent event ->
                    vLogEvent event
        )
        |> TsEncode.variantTagged "LogEvent" eventEncoder
        |> TsEncode.buildUnion

(TsEncode.encoder fromElm) (LogEvent { level = "info", message = "Hello" }) |> Json.Encode.encode 0
--> """{"tag":"LogEvent","data":{"level":"info","message":"Hello"}}"""

buildUnion : UnionBuilder (match -> UnionEncodeValue) -> Encoder match


type alias UnionEncodeValue =
TsJson.Internal.Encode.UnionEncodeValue

We can guarantee that you're only encoding to a given set of possible shapes in a union type by ensuring that all the encoded values come from the union pipeline, using functions like variantLiteral, variantObject, etc.

Applying another variant function in your union pipeline will give you more functions/values to give UnionEncodeValue's with different shapes, if you need them.

Collections

list : Encoder a -> Encoder (List a)

import Json.Encode as Encode

[ "Hello", "World!" ]
    |> runExample ( list string )
--> { output = """["Hello","World!"]"""
--> , tsType = "string[]"
--> }

dict : (comparableKey -> String) -> Encoder input -> Encoder (Dict comparableKey input)

import Json.Encode as Encode
import Dict

Dict.fromList [ ( "a", "123" ), ( "b", "456" ) ]
    |> runExample ( dict identity string )
--> { output = """{"a":"123","b":"456"}"""
--> , tsType = "{ [key: string]: string }"
--> }

tuple : Encoder input1 -> Encoder input2 -> Encoder ( input1, input2 )

TypeScript has a Tuple type. It's just an Array with 2 items, and the TypeScript compiler will enforce that there are two elements. You can turn an Elm Tuple into a TypeScript Tuple.

import Json.Encode as Encode

( "John Doe", True )
    |> runExample ( tuple string bool )
--> { output = """["John Doe",true]"""
--> , tsType = "[ string, boolean ]"
--> }

If your target Elm value isn't a tuple, you can map it into one

{ name = "John Smith", isAdmin = False }
    |> runExample
        (tuple string bool
            |> map
                (\{ name, isAdmin } ->
                    ( name, isAdmin )
                )
        )
--> { output = """["John Smith",false]"""
--> , tsType = "[ string, boolean ]"
--> }

triple : Encoder input1 -> Encoder input2 -> Encoder input3 -> Encoder ( input1, input2, input3 )

Same as tuple, but with Triples

import Json.Encode as Encode

( "Jane Doe", True, 123 )
    |> runExample ( triple string bool int )
--> { output = """["Jane Doe",true,123]"""
--> , tsType = "[ string, boolean, number ]"
--> }

maybe : Encoder a -> Encoder (Maybe a)

import Json.Encode as Encode

Just 42
    |> runExample ( maybe int )
--> { output = "42"
--> , tsType = "number | null"
--> }

array : Encoder a -> Encoder (Array a)

Like Encode.list, but takes an Array instead of a List as input.

In-Depth Example

You can use elm-ts-interop to build up Encoders that have the same TypeScript type as a web platform API expects. Here's an example that we could use to call the scrollIntoView method on a DOM Element.

import Json.Encode

type Behavior
    = Auto
    | Smooth

type Alignment
    = Start
    | Center
    | End
    | Nearest

scrollIntoViewEncoder : Encoder
        { behavior : Maybe Behavior
        , block : Maybe Alignment
        , inline : Maybe Alignment
        }
scrollIntoViewEncoder =
    object
        [ optional "behavior" .behavior behaviorEncoder
        , optional "block" .block alignmentEncoder
        , optional "inline" .inline alignmentEncoder
        ]

behaviorEncoder : Encoder Behavior
behaviorEncoder =
    union
        (\vAuto vSmooth value ->
            case value of
                Auto ->
                    vAuto
                Smooth ->
                    vSmooth
        )
        |> variantLiteral (Json.Encode.string "auto")
        |> variantLiteral (Json.Encode.string "smooth")
        |> buildUnion


alignmentEncoder : Encoder Alignment
alignmentEncoder =
    union
        (\vStart vCenter vEnd vNearest value ->
            case value of
                Start ->
                    vStart
                Center ->
                    vCenter
                End ->
                    vEnd
                Nearest ->
                    vNearest
        )
        |> variantLiteral (Json.Encode.string "start")
        |> variantLiteral (Json.Encode.string "center")
        |> variantLiteral (Json.Encode.string "end")
        |> variantLiteral (Json.Encode.string "nearest")
        |> buildUnion


{ behavior = Just Auto, block = Just Nearest, inline = Nothing }
        |> runExample scrollIntoViewEncoder
--> { output = """{"behavior":"auto","block":"nearest"}"""
--> , tsType = """{ behavior? : "smooth" | "auto"; block? : "nearest" | "end" | "center" | "start"; inline? : "nearest" | "end" | "center" | "start" }"""
--> }

Escape Hatch

value : Encoder Json.Encode.Value

This is an escape hatch that allows you to send arbitrary JSON data. The type will be JSON in TypeScript, so you won't have any specific type information. In some cases, this is fine, but in general you'll usually want to use other functions in this module to build up a well-typed Encoder.

Executing Encoders

Usually you don't need to use these functions directly, but instead the code generated by the elm-ts-interop command line tool will use these for you under the hood. These can be helpful for debugging, or for building new tools on top of this package.

encoder : Encoder input -> input -> Json.Encode.Value

tsType : Encoder input -> Internal.TsJsonType.TsType

Documentation Helper

runExample : Encoder input -> input -> { output : String, tsType : String }