Encoding Custom Types

  1. 1. What Is elm-ts-interop?
    2:57
  2. 2. Setup
    2:10
  3. 3. Flags
    2:52
  4. 4. FromElm Ports
    5:44
  5. 5. Encoding Custom Types
    9:54
  6. 6. Exhaustive Ports
    6:30
  7. 7. ToElm Ports
    11:10
  8. 8. Migrating to Pro
    9:32

Chapter Notes

  • elm-review-ports rule for NoUnusedPorts
  • Elm's dead code elimination can cause an error if you don't register ports. elm-ts-interop helps avoid this by using a single port-pair (interopToElm and interopFromElm).

TypeScript Literal types#

TypeScript Literal Types are useful for describing the shape of JSON. Since JSON doesn't have custom types or enums, literals greatly expand the expressive power of JSON.

Since Elm ports and flags are limited to JSON, elm-ts-interop relies heavily on the guarantees from TypeScript Literal Types.

An example of a Literal Type is a literal string. With this elm-ts-json Encoder:

encoder : Encoder Kind
encoder =
    TsEncode.literal (Json.Encode.string "hello!")

We get a TypeScript type "hello!". Notice that it is a type, not just a value.

Hands On with Literal Types#

ellie-app fQWYRPSQxvsa1

Union encoders#

TypeScript Union Types

Encodes into a TypeScript Union type. In TypeScript, Union types are "untagged unions". Elm's Custom Types are "tagged unions."

That means we can do

type IdStringOrNumber = IdString String | IdNumber Int

id : IdStringOrNumber
id = IdString "abc123"
type Id = string | number;

id: Id = "abc123";

Similar pattern to miniBill/elm-codec, the first argument is a function which has 1 argument for each variant encoder and 1 argument which is the value that we can do a case expression on.

In the case expression, we pick an encoder to use. The variant encoder parameters correspond to the pipeline below.

encoder : Encoder Kind
encoder =
    TsEncode.union
        (\vError vWarning vInfo vAlert value ->
            case value of
                Error ->
                    vError

                Warning ->
                    vWarning

                Info ->
                    vInfo

                Alert ->
                    vAlert
        )
        |> TsEncode.variantLiteral (Json.Encode.string "error")
        |> TsEncode.variantLiteral (Json.Encode.string "warn")
        |> TsEncode.variantLiteral (Json.Encode.string "info")
        |> TsEncode.variantLiteral (Json.Encode.string "alert")
        |> TsEncode.buildUnion

Type Narrowing#

The TypeScript compiler uses control flow analysis to narrow types. So if you check the type of a value in a conditional, the TypeScript compiler is aware of the type information within the conditional branches.

// logKind's type is 'error' | 'warning' | 'info' | 'alert'
if (fromElm.data.logKind === "alert") {
  // logKind's type is 'alert'
} else {
  // logKind's type is 'error' | 'warning' | 'info'
}

TypeScript's Discriminated Union functionality uses Type Narrowing to get functionality like Elm's case expressions for Custom Types.

Source of Truth#

Serialization for an Encoder, Deserialization for a Decoder. Allows us to decouple type information. The types don't have to match in Elm and TypeScript. But we can safely change our Encoders and Decoders because the type information will always be in sync with elm-ts-interop.