dillonkearns/elm-ts-json/1.0.0

NOTE: the latest version is 2.1.3

TsJson.Decode

The TsJson.Decode module is what you use for

  • Flags
  • ToElm Ports

By building a Decoder with this API, you're also describing the source of truth for how to take a TypeScript type and turn it into an Elm type. Note that there is complete type information here just by using this API (no magic parsing or static analysis needed).

Let's take this example:

import Json.Decode




int
    |> map String.fromInt
    |> runExample "1000"
--> { decoded = Ok "1000"
--> , tsType = "number"
--> }

In this example, there are no differences from how we would write this with the elm/json API. Let's consider the type information from two different sides: the Elm types (which would be used in an Elm type annotation), and the TypeScript types (which show up in the TypeScript Declaration file that elm-ts-interop generates to gives you nice autocompletion and type information).

The Elm type information

  • The initial Decoder, TsJson.Decode.int, has the Elm type TsJson.Decode.Decoder Int. So Elm knows this Decoder will either fail, or give us an Int.
  • When we call TsJson.Decode.map String.fromInt, the Elm type information changes. We're mapping with String.fromInt : String -> Int. So that means we'll now decode into an Elm String instead of an Int. And that's the final Elm type we'll end up with.

The TypeScript type information

  • TsJson.Decode.int expects a number from TypeScript.
  • TsJson.Decode.map applies a function to the decoded Elm value, but it doesn't change what we expect from TypeScript. So we still expect a number from TypeScript. And we're done, so that's the final type we expect to receive from TypeScript.

Summary

We can apply more Decoders, like TsJson.Decode.list, for example, to expect an array of that type from TypeScript, and a List of that Elm type. From there, it's just the same concept. All the type information about the type that Elm will decode into, and the type that Elm expects from TypeScript, is built up as you build a Decoder.

That means that the source of truth is the Decoder itself. Note that the Decoder doesn't specify just the Elm format, or just the TypeScript type as the source of truth. It specifies how to turn a TypeScript type into an Elm type as the source of truth. That means that

  • You can change the decoder to change either type independently
  • The two sides (Elm and TS) will always be in sync (as long as you are re-running the elm-ts-interop CLI at the appropriate time)

Decoders

type Decoder value

Just like a Decoder in elm/json, except these Decoders track the TypeScript types that they can successfully handle.

succeed : value -> Decoder value
import Json.Decode




succeed "abcdefg"
    |> runExample "12345"
--> { decoded = Ok "abcdefg"
--> , tsType = "JsonValue"
--> }
fail : String -> Decoder value
import Json.Decode




fail "Failure message"
    |> runExample "123.45"
--> { decoded = Err "Problem with the given value:\n\n123.45\n\nFailure message"
--> , tsType = "JsonValue"
--> }

Built-Ins

bool : Decoder Bool
import Json.Decode




bool
    |> runExample "true"
--> { decoded = Ok True
--> , tsType = "boolean"
--> }
float : Decoder Float
import Json.Decode




float
    |> runExample "1.23"
--> { decoded = Ok 1.23
--> , tsType = "number"
--> }
int : Decoder Int
import Json.Decode




int
    |> runExample "1000"
--> { decoded = Ok 1000
--> , tsType = "number"
--> }


Floating point values will cause a decoding error.


int
    |> runExample "1.23"
--> { decoded = Err "Problem with the given value:\n\n1.23\n\nExpecting an INT"
--> , tsType = "number"
--> }
string : Decoder String
import Json.Decode




string
    |> runExample """ "Hello!" """
--> { decoded = Ok "Hello!"
--> , tsType = "string"
--> }

Objects

field : String -> Decoder value -> Decoder value
import Json.Decode




field "first" string
    |> runExample """{"first":"James","middle":"Tiberius","last":"Kirk"}"""
--> { decoded = Ok "James"
--> , tsType = "{ first : string }"
--> }
at : List String -> Decoder value -> Decoder value
import Json.Decode
import Json.Encode


type Mode
    = DarkMode
    | LightMode


modeDecoder : Decoder Mode
modeDecoder =
    oneOf [ literal DarkMode <| Json.Encode.string "dark", literal LightMode <| Json.Encode.string "light" ]
        |> (at [ "options", "mode" ])


modeDecoder
    |> runExample """{
                       "options": { "mode": "dark" },
                       "version": "1.2.3"
                     }"""
--> { decoded = Ok DarkMode
--> , tsType = """{ options : { mode : "dark" | "light" } }"""
--> }

Composite Types

list : Decoder value -> Decoder (List value)
import Json.Decode




list int
    |> runExample "[1,2,3]"
--> { decoded = Ok [ 1, 2, 3 ]
--> , tsType = "number[]"
--> }
array : Decoder value -> Decoder (Array value)

Exactly the same as the list Decoder except that it wraps the decoded List into an Elm Array.

nullable : Decoder value -> Decoder (Maybe value)
import Json.Decode




nullable int |> runExample "13"
--> { decoded = Ok (Just 13)
--> , tsType = "number | null"
--> }


nullable int |> runExample "null"
--> { decoded = Ok Nothing
--> , tsType = "number | null"
--> }


nullable int |> runExample "true"
--> { decoded = Err "Json.Decode.oneOf failed in the following 2 ways:\n\n\n\n(1) Problem with the given value:\n    \n    true\n    \n    Expecting null\n\n\n\n(2) Problem with the given value:\n    \n    true\n    \n    Expecting an INT"
--> , tsType = "number | null"
--> }
oneOf : List (Decoder value) -> Decoder value

You can express quite a bit with oneOf! The resulting TypeScript types will be a Union of all the TypeScript types for each Decoder in the List.

import Json.Decode
import Json.Encode


list ( oneOf [ int |> map toFloat, float ] )
    |> runExample "[1, 2, 3.14159, 4]"
--> { decoded = Ok [1.0, 2.0, 3.14159, 4.0]
--> , tsType = """(number | number)[]"""
--> }
dict : Decoder value -> Decoder (Dict String value)
import Json.Decode
import Json.Encode
import Dict exposing (Dict)




dict int
    |> runExample """{"alice":42,"bob":99}"""
--> { decoded = Ok (Dict.fromList [ ( "alice", 42 ), ( "bob", 99 ) ])
--> , tsType = "{ [key: string]: number }"
--> }
keyValuePairs : Decoder value -> Decoder (List ( String, value ))
import Json.Decode




keyValuePairs int
    |> runExample """{ "alice": 42, "bob": 99 }"""
--> { decoded = Ok [ ( "alice", 42 ), ( "bob", 99 ) ]
--> , tsType = "{ [key: string]: number }"
--> }
oneOrMore : (a -> List a -> value) -> Decoder a -> Decoder value
import Json.Decode
import Json.Encode




oneOrMore (::) int
    |> runExample "[12345]"
--> { decoded = Ok [ 12345 ]
--> , tsType = """[ number, ...(number)[] ]"""
--> }


type TestResult
    = Pass
    | Fail String


testCaseDecoder : Decoder TestResult
testCaseDecoder =
    oneOf [
        field "tag" (literal Pass (Json.Encode.string "pass"))
      , map2 (\() message -> Fail message)
          ( field "tag" (literal () (Json.Encode.string "fail")) )
          ( field "message" string )
    ]


oneOrMore (::) testCaseDecoder
    |> runExample """[ { "tag": "pass" } ]"""
--> { decoded = Ok [ Pass ]
--> , tsType = """[ { tag : "pass" } | { tag : "fail"; message : string }, ...({ tag : "pass" } | { tag : "fail"; message : string })[] ]"""
--> }
optionalField : String -> Decoder value -> Decoder (Maybe value)

This is a safer (and more explicit) way to deal with optional fields compared to maybe. It may seem that wrapping a field Decoder in a maybe Decoder achieves the same behavior, but the key difference is that the maybe version will convert a failing decoder on a value that is present into Nothing, as if it wasn't present. Often what you want is for the malformed version to fail, which is exactly what this function will do.

import Json.Decode




json : String
json = """{ "name": "tom", "age": null }"""


optionalField "height" float
    |> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ height? : number }"
--> }


optionalField "age" int
    |> runExample json
--> { decoded = Err "Problem with the value at json.age:\n\n    null\n\nExpecting an INT"
--> , tsType = "{ age? : number }"
--> }
optionalNullableField : String -> Decoder value -> Decoder (Maybe value)
import Json.Decode




json : String
json = """{ "name": "tom", "age": null }"""


optionalNullableField "height" float
    |> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ height? : number | null }"
--> }


optionalNullableField "age" int
    |> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ age? : number | null }"
--> }
index : Int -> Decoder value -> Decoder value
import Json.Decode




index 1 int
    |> runExample "[0,100,200]"
--> { decoded = Ok 100
--> , tsType = "[JsonValue,number,...JsonValue[]]"
--> }


map2 Tuple.pair
    ( index 1 int )
    ( index 3 string )
    |> runExample """[0,100,"a","b"]"""
--> { decoded = Ok ( 100, "b" )
--> , tsType = "[JsonValue,number,JsonValue,string,...JsonValue[]]"
--> }
tuple : Decoder value1 -> Decoder value2 -> Decoder ( value1, value2 )
import Json.Decode




tuple string int
    |> runExample """["abc", 123]"""
--> { decoded = Ok ( "abc", 123 )
--> , tsType = "[ string, number ]"
--> }
Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder ( value1, value2, value3 )
import Json.Decode




triple string int bool
    |> runExample """["abc", 123, true]"""
--> { decoded = Ok ( "abc", 123, True )
--> , tsType = "[ string, number, boolean ]"
--> }

Arbitrary-Length Tuples

TypeScript allows you to define a tuple that can have any length of items with specific types.

TypeScript tuples are much like an Elm tuples, except two key differences:

  • Elm tuples can only have 2 or 3 items, while TypeScript tuples can have any length
  • Elm tuples are a distinct type, while TypeScript tuples are just arrays with a statically known length
type TupleBuilder value
import Json.Decode




startTuple (\a b c d -> { a = a, b = b, c = c, d = d })
    |> element string
    |> element int
    |> element bool
    |> element string
    |> buildTuple
    |> runExample """["abc", 123, true, "xyz"]"""
--> { decoded = Ok { a = "abc", b = 123, c = True, d = "xyz" }
--> , tsType = "[ string, number, boolean, string ]"
--> }

Transformations

map : (value -> mapped) -> Decoder value -> Decoder mapped
import Json.Decode




int
    |> map String.fromInt
    |> runExample "1000"
--> { decoded = Ok "1000"
--> , tsType = "number"
--> }

Combining

(value1 -> value2 -> mapped)
-> Decoder value1
-> Decoder value2
-> Decoder mapped

You can use map2, map3, etc. to build up decoders that have somewhat clearer error messages if something goes wrong. Some people prefer the pipeline style using andMap as it has fewer parentheses and you don't have to change the number when you add a new field. It's a matter of personal preference.

import Json.Decode




type alias Country = { name : String, populationInMillions : Int }


map2 Country
    (field "name" string)
    (field "population" (int |> map (\totalPopulation -> floor (toFloat totalPopulation / 1000000.0))))
    |> runExample """ {"name": "Norway", "population":5328000} """
--> { decoded = Ok { name = "Norway", populationInMillions = 5 }
--> , tsType = "{ name : string; population : number }"
--> }
andMap : Decoder a -> Decoder (a -> b) -> Decoder b

This is useful for building up a decoder with multiple fields in a pipeline style. See https://github.com/elm-community/json-extra/blob/2.0.0/docs/andMap.md.

import Json.Decode




type alias Country = { name : String, populationInMillions : Int }


succeed Country
    |> andMap (field "name" string)
    |> andMap (field "population" (int |> map (\totalPopulation -> floor (toFloat totalPopulation / 1000000.0))))
    |> runExample """ {"name": "Norway", "population":5328000} """
--> { decoded = Ok { name = "Norway", populationInMillions = 5 }
--> , tsType = "{ population : number; name : string }"
--> }

TypeScript Literals

literal : value -> Value -> Decoder value

TypeScript has support for literals.

import Json.Decode
import Json.Encode as JE




literal () (JE.string "unit")
    |> runExample """ "unit" """
--> { decoded = Ok ()
--> , tsType = "\"unit\""
--> }
null : value -> Decoder value
import Json.Decode




null False
    |> runExample "null"
--> { decoded = Ok False
--> , tsType = "null"
--> }

Continuation

AndThenContinuation (value -> Decoder decodesTo)
-> Decoder value
-> Decoder decodesTo
import Json.Decode




example : AndThenContinuation (Int -> Decoder String)
example =
    andThenInit
        (\v1Decoder v2PlusDecoder version ->
            case version of
                1 -> v1Decoder
                _ -> v2PlusDecoder
        )
        |> andThenDecoder (field "payload" string)
        |> andThenDecoder (at [ "data", "payload" ] string)




field "version" int |> andThen example
    |> runExample """{"version": 1, "payload": "Hello"}"""
--> { decoded = Ok "Hello"
--> , tsType = "({ version : number } & { data : { payload : string } } | { payload : string })"
--> }

This type allows you to combine all the possible Decoders you could run in an andThen continuation.

This API allows you to define all possible Decoders you might use up front, so that all possible TypeScript types the continuation could decode are known after building up the decoder instead of after running the decoder.

elm/json Decoder Escape Hatches

If you have an existing decoder, you can use it with an unknown type in TypeScript.

You can also decode an arbitrary JSON value as with elm/json, and then use elm/json to process it further.

value : Decoder Value

Gives you an escape hatch to decode to a plain elm/json Json.Decode.Value. This has the TypeScript type unknown. Avoid using this when possible.

import Json.Decode




value
    |> runExample "Hello"
--> { decoded = (Json.Decode.decodeString Json.Decode.value "Hello" |> Result.mapError Json.Decode.errorToString)
--> , tsType = "JsonValue"
--> }
unknownAndThen : (a -> Decoder b) -> Decoder a -> Decoder b

If you need to run a regular JSON Decoder in an andThen, you can use this function, but it will yield a Decoder with an unknown TypeScript type.

import Json.Decode as JD




field "version" int |> unknownAndThen (\versionNumber ->
    if versionNumber == 1 then
      JD.field "payload" JD.string
    else
      JD.at [ "data", "payload" ] JD.string
)
    |> runExample """{"version": 1, "payload": "Hello"}"""
--> { decoded = Ok "Hello"
--> , tsType = "JsonValue"
--> }
maybe : Decoder value -> Decoder (Maybe value)

This function is somewhat risky in that it could cover up a failing Decoder by turning it into a Nothing. In some cases, this may be what you're looking for, but if you're trying to deal with optional fields, it's safer to use optionalField and it will give you better type information. See the thisShouldBeABoolean example below. In that example, we're decoding a JSON value which should have been a boolean but instead is a string. We'd like the Decoder to fail to let us know it wasn't able to process the value correctly, but instead it covers up the failure and decodes to Nothing.

So use this Decoder with care!

import Json.Decode




json : String
json = """{ "name": "tom", "age": 42, "thisShouldBeABoolean": "true" }"""




-- WARNING: uh oh, this may not be the desired behavior!
maybe (field "thisShouldBeABoolean" bool)
    |> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ thisShouldBeABoolean : boolean } | JsonValue"
--> }


maybe (field "height" float)
    |> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ height : number } | JsonValue"
--> }


field "height" (maybe float)
    |> runExample json
--> { decoded = Err "Problem with the given value:\n\n{\n        \"name\": \"tom\",\n        \"age\": 42,\n        \"thisShouldBeABoolean\": \"true\"\n    }\n\nExpecting an OBJECT with a field named `height`"
--> , tsType = "{ height : number | JsonValue }"
--> }

Using Decoders

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.

decoder : Decoder value -> Decoder value

Get a regular JSON Decoder that you can run using the elm/json API.

tsType : Decoder value -> TsType

mapN for Combining More Than Two Items

(value1 -> value2 -> value3 -> mapped)
-> Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder mapped
(value1 -> value2 -> value3 -> value4 -> mapped)
-> Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder value4
-> Decoder mapped
(value1 -> value2 -> value3 -> value4 -> value5 -> mapped)
-> Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder value4
-> Decoder value5
-> Decoder mapped
(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> mapped)
-> Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder value4
-> Decoder value5
-> Decoder value6
-> Decoder mapped
( value1
-> value2
-> value3
-> value4
-> value5
-> value6
-> value7
-> mapped
)
-> Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder value4
-> Decoder value5
-> Decoder value6
-> Decoder value7
-> Decoder mapped
( value1
-> value2
-> value3
-> value4
-> value5
-> value6
-> value7
-> value8
-> mapped
)
-> Decoder value1
-> Decoder value2
-> Decoder value3
-> Decoder value4
-> Decoder value5
-> Decoder value6
-> Decoder value7
-> Decoder value8
-> Decoder mapped

Internals

String
-> Decoder value
-> { decoded : Result String value
, tsType : String
}