# Generating ports with elm-ts-interop

**URL:** <https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436>\
**Category:** Learn\
**Created:** [June 6, 2021, 11:44am UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436 "2021-06-06T11:44:43Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![124C41](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/124c41/32/4115_2.png) [@124C41](https://discourse.elm-lang.org/u/124C41)\
**Post date:** [June 6, 2021, 11:44am UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/1 "2021-06-06T11:44:43Z")

</div>

Recently, I tried out `elm-ts-interop` (v0.0.3 community edition). I suppose it is still under development since there seems to be no specific documentation (except for pro edition) or source code available, but I really like what I have seen so far. There are just a few points confusing me:

If I understand things correctly, `elm-ts-interop` requires me to write an elm-module (InteropDefinitions.elm say) exposing a function

`interop : { fromElm : Encoder input, toElm : Decoder output, flags : Decoder flags }`.

Given that module, `elm-ts-interop` generates a typescript type declaration file specifying exactly two ports called `interopFromElm` and `interopToElm`. On elm side, there is nothing generated - I have to write these two ports manually. Although they behave correctly at runtime, I have to pay attention on typing their names correctly, since wrong typings will not cause any exceptions (neither at compile time, nor at runtime).

**Question 1:** Why does `elm-ts-interop` generate type declarations for only these two specific ports? Is there any possibility to specify other ports?

**Question 2:** The module InteropDefinitions.elm is thought as single point of truth for both sides - typescript and elm. Wouldn’t it be consequent to generate an elm module containing the port declarations as well?

---

<div class="post-metadata">

**Author:** ![dillonkearns](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dillonkearns/32/1587_2.png) [@dillonkearns](https://discourse.elm-lang.org/u/dillonkearns)\
**Post date:** [June 6, 2021, 4:34pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/2 "2021-06-06T16:34:15Z")

</div>

> [@124C41](#):
>
> Recently, I tried out `elm-ts-interop` (v0.0.3 community edition). I suppose it is still under development since there seems to be no specific documentation (except for pro edition) or source code available, but I really like what I have seen so far.

Apologies for the lack of documentation for the community edition! This is a high priority for me, and I will definitely be working hard to get some nice and thorough documentation for `elm-ts-interop` Community Edition once I get the `elm-pages` 2.0 release out!

> [@124C41](#):
>
> **Question 1:** Why does `elm-ts-interop` generate type declarations for only these two specific ports? Is there any possibility to specify other ports?

If there were more ports, then that would be more points of failure that don’t get the benefit of type-safety. The idea is that `elm-ts-interop` uses a single port pair (to and from Elm) because then you can use those ports to handle all the incoming or outgoing messages you need to with type-safety.

This is similar to the advice given in Murphy Randle’s Elm Conf talk about ports

[https://www.youtube.com/watch?v=P3pL85n9\_5s](https://www.youtube.com/watch?v=P3pL85n9_5s)

One of the main features of the pro version is to make it a little more convenient to use this style because it takes top-level definitions like this:

```elm
module InteropDefinitions exposing (logInfo, logWarning)

import TsJson.Encode as TsEncode exposing (object, required)

logInfo : TsEncode.Encoder String
logInfo =
    object [required "message" identity string]

logWarning : TsEncode.Encoder String
logWarning =
    object [required "message" identity string]

```

And it gives you some helper functions to call them more conveniently:

```elm
-- exposed functions in generated InteropPorts module
InteropPorts.logInfo : String -> Cmd msg
InteropPorts.logWarning : String -> Cmd msg

```

Under the hood, the pro version generates the equivalent of the top-level glue code that you would manually do like this in the community edition:

```elm
module InteropDefinitions exposing (Flags, FromElm(..), ToElm, interop)

import Json.Decode as JD
import Json.Encode as JE
import TsJson.Decode as Decode exposing (Decoder)
import TsJson.Encode as Encoder exposing (Encoder, optional, required)

interop : { toElm : Decoder ToElm, fromElm : Encoder FromElm, flags : Decode.Decoder Flags }
interop =
    { toElm = Decode.null ()
    , fromElm = fromElm
    , flags = Decode.null ()
    }

type FromElm
    = LogInfo String
    | LogWarning String

type alias ToElm = ()

type alias Flags = ()

fromElm : Encoder.Encoder FromElm
fromElm =
    Encoder.union
        (\vLogInfo vLogWarning value ->
            case value of
                LogInfo string ->
                    vLogInfo string

                LogWarning string ->
                    vLogWarning string
        )
        |> Encoder.variantObject "LogInfo" [required "message" identity Encoder.string]
        |> Encoder.variantObject "LogWarning" [required "message" identity Encoder.string]
        |> Encoder.buildUnion

```

So the core goal is type-safety, and the design of a single port pair is with that goal in mind. The community version gives you that type-safety, and the pro version adds some advanced code generation features that give some extra convenience for using those.

> [@124C41](#):
>
> **Question 2:** The module InteropDefinitions.elm is thought as single point of truth for both sides - typescript and elm. Wouldn’t it be consequent to generate an elm module containing the port declarations as well?

That’s absolutely correct that the definitions module is the point of truth for both sides (TS and Elm). The pro version does some code generation to give some convenience functions, like I described above, so it does generate the ports within that generated code, and it doesn’t expose the ports so they can only be called indirectly through the exposed convenience functions.

The community edition could generate code, but it’s only a single generated module that doesn’t ever change so I think of it more as a scaffolded file than generated code. I’ve considered adding a command to copy the scaffolded module somewhere, but all it would do is copy paste a file like this:

```elm
port module GeneratedPorts exposing (decodeFlags, fromElm, toElm)

import InteropDefinitions
import Json.Decode
import Json.Encode
import TsJson.Decode as Decode
import TsJson.Encode as Encode

fromElm : InteropDefinitions.FromElm -> Cmd msg
fromElm value =
    value
        |> (InteropDefinitions.interop.fromElm |> Encode.encoder)
        |> interopFromElm

toElm : Sub msg
toElm =
    (InteropDefinitions.interop.toElm |> Decode.decoder)
        |> Json.Decode.decodeValue
        |> interopToElm

decodeFlags : Json.Decode.Value -> Result Json.Decode.Error InteropDefinitions.Flags
decodeFlags flags =
    Json.Decode.decodeValue
        (InteropDefinitions.interop.flags |> Decode.decoder)
        flags

port interopFromElm : Json.Encode.Value -> Cmd msg

port interopToElm : (Json.Decode.Value -> msg) -> Sub msg

```

I was weighing the pros and cons of having docs for how to run a command to copy a file like that, versus just having a file like that and instructing people to copy it. I’m open to feedback there, but essentially that’s some of the basic boilerplate that needs to be done one way or another for setting up the `elm-ts-interop` wiring.

I think it’s worth emphasizing that the `interopFromElm` and `interopToElm` ports are not exposed in this module. This is important because it means that you know they are being used only through the `TsJson.Decode`/`Encode` APIs, so you get that type-safe contract.

I hope that helps answer those questions! Stay tuned for some docs, and let me know how it goes otherwise, I love to hear feedback.

---

<div class="post-metadata">

**Author:** ![124C41](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/124c41/32/4115_2.png) [@124C41](https://discourse.elm-lang.org/u/124C41)\
**Post date:** [June 9, 2021, 10:10am UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/3 "2021-06-09T10:10:22Z")

</div>

Dear @dillonkearns,

thank you very much for your extremely detailed clarifications which answer my questions to their full extent!

Just to sum things up in my own words:  
Instead of generating arbitrarily many ports, `etm-ts-interop` only generes one port in each direction which can be used as multiplexer. For communication between ts an elm we could use, for example, objects of the form `{ messageType, payload }`. At the receiving side, we implement a switch-case-block on `messageType`, and by typesystem magic, the corresponding programming language (ts or elm) knows the precise type of `payload`, given any concrete `messageType`.

---

<div class="post-metadata">

**Author:** ![dillonkearns](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dillonkearns/32/1587_2.png) [@dillonkearns](https://discourse.elm-lang.org/u/dillonkearns)\
**Post date:** [June 9, 2021, 4:20pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/4 "2021-06-09T16:20:13Z")

</div>

That’s correct! Glad that explanation helped.

Feel free to ask questions any time either here or in the elm-ts-interop slack and I’ll do my best to help!

---

<div class="post-metadata">

**Author:** ![124C41](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/124c41/32/4115_2.png) [@124C41](https://discourse.elm-lang.org/u/124C41)\
**Post date:** [June 10, 2021, 10:34am UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/5 "2021-06-10T10:34:42Z")

</div>

@dillonkearns I have indeed a follow up question:

My current goal is to write some general purpose multiplexer helper functions. The to-elm-part was not too hard:

```auto
module InteropDefinitions exposing (interop)

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

type ToElm
    = SomeInput Int
    | OtherInput String

toElmMux : List ( String, Decoder toElm ) -> Decoder toElm
toElmMux =
    List.map
        (\( name, decoder ) ->
            Decode.field "tag" (Decode.literal identity (JE.string name))
                |> Decode.andMap (Decode.field "payload" decoder)
        )
        >> Decode.oneOf

interop =
    { toElm =
        toElmMux
            [ ( "SomeInput", Decode.map SomeInput Decode.int )
            , ( "OtherInput", Decode.map OtherInput Decode.string )
            ]
    , fromElm = ...
    , flags = ...
    }

```

This `toElmMux` function really makes my life easier since it lets me add further ports without remembering the implementation details of multiplexing.

Now I would like to implement an analogous function `fromElmMux` which seems a lot harder. I am not even sure what its signature should look like. A function of type

```auto
fromElmMux : List (String, fromElm -> Bool, Encoder fromElm ) -> Encoder fromElm

```

should do the job, but I don’t know if this would become handy in practise since elm seems not to support inline pattern matching (is it possible to write inline `fromElm -> Bool` guards if `fromElm` is a union type?).

I would love to hear your thoughts on this issue.

---

<div class="post-metadata">

**Author:** ![124C41](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/124c41/32/4115_2.png) [@124C41](https://discourse.elm-lang.org/u/124C41)\
**Post date:** [June 10, 2021, 12:22pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/6 "2021-06-10T12:22:45Z")

</div>

@dillonkearns Yet another follow up question: Is there any way to subscribe to single (to-elm) ports individually, given the multiplexing approach sketched abobve?

At the moment, I only succeeded in subscribing to all ports at once. As you suggested above, I implemented a function `toElm` which would be exposed instead of the actual port:

```auto
toElm : Sub (Result Json.Decode.Error ToElm)
toElm =
    (interop.toElm |> Decode.decoder)
        |> Json.Decode.decodeValue
        |> interopToElm

port interopToElm : (Json.Decode.Value -> msg) -> Sub msg

```

In my `Main.elm`-file I subscribed to the `toElm` function in the following way:

```auto
type Msg
    = NoOp
    | NewNumber Int
    | NewString String

subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.map
        (\result ->
            case result of
                Ok toElm ->
                    case toElm of
                        SomeInput x ->
                            NewNumber x

                        OtherInput x ->
                            NewString x

                Err _ ->
                    NoOp
        )
        toElm

```

This is working, however, it seems way too cumbersome to me.

Is there any way to implement functions

```auto
someInput : Sub Int
otherInput : Sub String

```

which can be subscribed to individually?

---

<div class="post-metadata">

**Author:** ![124C41](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/124c41/32/4115_2.png) [@124C41](https://discourse.elm-lang.org/u/124C41)\
**Post date:** [June 16, 2021, 4:41pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/7 "2021-06-16T16:41:48Z")

</div>

After rethinking for a while, I realized why building a multiplex-encoder has to be a lot more complicated then a corresponding decoder. Nevertheless, I’ve implemented some general purpose helper functions for building (de-)multiplexing encoders/decoders which help keeping the `InteropDefinitions.elm` file clean.

```auto
port module InteropHelpers exposing
    ( MuxBuilder
    , MuxValue
    , PayloadDecoder(..)
    , buildMux
    , demux
    , elm2tsInterop
    , emptyMux
    , initMux
    , loadedMux
    , ts2elmInterop
    )

import Json.Decode as JD
import Json.Encode as JE
import TsJson.Decode as Decode exposing (Decoder)
import TsJson.Encode as Encode exposing (Encoder, UnionBuilder, UnionEncodeValue, buildUnion)

-- multiplexing

type MuxBuilder a
    = MuxBuilder (UnionBuilder a)

type alias MuxValue =
    UnionEncodeValue

initMux : constructor -> MuxBuilder constructor
initMux =
    Encode.union >> MuxBuilder

loadedMux : String -> Encoder val -> MuxBuilder ((val -> MuxValue) -> tail) -> MuxBuilder tail
loadedMux name encoder (MuxBuilder builder) =
    Encode.variantObject name [Encode.required "payload" identity encoder] builder
        |> MuxBuilder

emptyMux : String -> MuxBuilder (MuxValue -> tail) -> MuxBuilder tail
emptyMux name (MuxBuilder builder) =
    Encode.variant0 name builder
        |> MuxBuilder

buildMux : MuxBuilder (fromElm -> MuxValue) -> Encoder fromElm
buildMux (MuxBuilder builder) =
    buildUnion builder

-- demultiplexing

type PayloadDecoder toElm
    = NoPayload toElm
    | WithPayload (Decoder toElm)

demux : List ( String, PayloadDecoder toElm ) -> Decoder toElm
demux =
    List.map
        (\( name, payloadDecoder ) ->
            Decode.field "tag" (Decode.literal identity (JE.string name))
                |> Decode.andMap
                    (case payloadDecoder of
                        NoPayload result ->
                            Decode.succeed result

                        WithPayload decoder ->
                            Decode.field "payload" decoder
                    )
        )
        >> Decode.oneOf

-- interop2elm

ts2elmInterop : Decoder toElm -> msg -> (toElm -> msg) -> Sub msg
ts2elmInterop decoder noop toMsg =
    Decode.decoder decoder
        |> JD.decodeValue
        |> interopToElm
        |> Sub.map
            (\result ->
                case result of
                    Ok data ->
                        toMsg data

                    Err _ ->
                        noop
            )

port interopToElm : (JD.Value -> msg) -> Sub msg

-- interop2ts

elm2tsInterop : Encoder fromElm -> fromElm -> Cmd msg
elm2tsInterop encoder =
    Encode.encoder encoder
        >> interopFromElm

port interopFromElm : JE.Value -> Cmd msg

```

This module is completely static and could theoretically be included into the elm-ts-json library (although you probably want to avoid redundancy). My actual `InteropDefinitions.elm` file now looks quite clean and readable.

```auto
module InteropDefinitions exposing (Flags, interop, receiveFromTS, sendSomethingElseToTS, sendSomethingToTS)

import InteropHelpers exposing (PayloadDecoder(..), buildMux, demux, elm2tsInterop, emptyMux, initMux, loadedMux, ts2elmInterop)
import TsJson.Decode as Decode exposing (Decoder)
import TsJson.Encode as Encode exposing (Encoder)

type alias Flags =
    ()

type ToElm
    = SomeInput Int
    | OtherInput

type FromElm
    = SomeOutput String
    | OtherOutput

interop : { toElm : Decoder ToElm, fromElm : Encoder FromElm, flags : Decoder Flags }
interop =
    { toElm =
        demux
            [ ( "SomeInput", WithPayload (Decode.int |> Decode.map SomeInput) )
            , ( "OtherInput", NoPayload OtherInput )
            ]
    , fromElm =
        initMux
            (\some other value ->
                case value of
                    SomeOutput x ->
                        some x

                    OtherOutput ->
                        other
            )
            |> loadedMux "SomeOutput" Encode.string
            |> emptyMux "OtherOutput"
            |> buildMux
    , flags = Decode.null ()
    }

-- interop functions to be exposed

sendSomethingToTS : String -> Cmd msg
sendSomethingToTS text =
    elm2tsInterop interop.fromElm (SomeOutput text)

sendSomethingElseToTS : Cmd msg
sendSomethingElseToTS =
    elm2tsInterop interop.fromElm OtherOutput

receiveFromTS : msg -> (Int -> msg) -> msg -> Sub msg
receiveFromTS noop someHandler otherHandler =
    ts2elmInterop interop.toElm
        noop
        (\val ->
            case val of
                SomeInput num ->
                    someHandler num

                OtherInput ->
                    otherHandler
        )

```

---

<div class="post-metadata">

**Author:** ![dillonkearns](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/dillonkearns/32/1587_2.png) [@dillonkearns](https://discourse.elm-lang.org/u/dillonkearns)\
**Post date:** [June 17, 2021, 5:58pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/8 "2021-06-17T17:58:58Z")

</div>

> [@124C41](#):
>
> Is there any way to implement functions
> 
> ```auto
> someInput : Sub Int
> otherInput : Sub String
> 
> ```
> 
> which can be subscribed to individually?

There’s an inherent complexity which is that TypeScript is not a sound type system like Elm’s, it is merely a guide. It gives you added safety, but I give the option to handle the error case since it’s still possible to bypass the TypeScript type system. You could create application-specific helpers to ignore the error case, or send it to an error tracker, etc., depending on your use case and application setup.

For subscribing to an individual toElm port, you would still need to handle that error case in some way, so it can be nice to handle all the ports in one place so you don’t have to handle the `Err` case more than once. There are benefits to picking out the ports in a single place, similar to the benefits of having a `case` expression for an `update` function.

You could create a helper to ignore all other cases, but I always try to make safe abstractions the top priority, and then convenience helpers that may be less safe can always be derived (for example, ignoring `Err` cases or potentially ignoring other ports and forgetting to handle them).

Hopefully that helps a little bit!

---

<div class="post-metadata">

**Author:** ![124C41](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/124c41/32/4115_2.png) [@124C41](https://discourse.elm-lang.org/u/124C41)\
**Post date:** [June 18, 2021, 1:54pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/9 "2021-06-18T13:54:34Z")

</div>

> [@dillonkearns](#):
>
> Hopefully that helps a little bit!

Absolutely, that’s very convincing!

On top of that, I found an old discussion somewhere, whether the `Sub` type deserves a filter function or not. It seems that nowadays, it is actually impossible to filter subscriptions (without sending `NoOp` messages) in elm. So there is no meaningful way at all to convert `Sub ToElm` to `Sub Int`.

---

<div class="post-metadata">

**Author:** ![system](https://canada1.discourse-cdn.com/flex035/uploads/elm_lang/original/1X/50a05e53677a2c3b47776d7abd0f113eb50193a1.png) [@system](https://discourse.elm-lang.org/u/system)\
**Post date:** [June 28, 2021, 1:55pm UTC](https://discourse.elm-lang.org/t/generating-ports-with-elm-ts-interop/7436/10 "2021-06-28T13:55:32Z")

</div>

Closed on June 28, 2021, 1:55pm UTC

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.
