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.
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
:
elm/json
Json.Encode.Value
- a value representing an encoded JSON valueelm-ts-interop
TsJson.Encode.Encoder
- a function for turning an Elm value into an encoded JSON value. The Encoder
itself has a definite TypeScript type, before you even pass in an Elm value to turn into JSON.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 }"
--> }
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"
--> }
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"
--> }
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 }"
--> }
optional : String -> (input -> Maybe mappedInput) -> Encoder mappedInput -> Property input
required : String -> (input -> mappedInput) -> Encoder mappedInput -> Property input
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" }"""
--> }
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
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.
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.
You can use elm-ts-interop
to build up Encoder
s 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" }"""
--> }
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
.
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
runExample : Encoder input -> input -> { output : String, tsType : String }