Announcing elm-duet 0.1.0

TL;DR: new tool for Elm/TS interop. Get it at GitHub - BrianHicks/elm-duet.


Hey all!

I’ve been working on a new way to type interop between Elm and TypeScript, and as of today I think it’s ready to foist upon an unsuspecting world release the first version!

The basic idea here is to define a schema with JTD, which gives a really nice subset of both the Elm and TypeScript type systems. Then you can use that to generate both Elm and TypeScript types and keep them in sync without having to maintain either side by hand[1].

You can get source and binaries at GitHub - BrianHicks/elm-duet, and here’s a sample schema that gives you a taste of what the tool can do (in YAML syntax to make comments easier):

# An example schema that uses JWTs to manage authentication. Imagine that the
# JWTs are stored in localStorage so that they can persist across sessions. The
# lifecycle of this app might look like:
#
# 1. On init, the JS side passes the Elm runtime the current value of the JWT
#    (or `null`, if unset)
# 2. Elm is responsible for authentication and for telling JS when it gets a
#    new JWT (for example, when the user logs in)

# To start, we'll define a "jwt" that will just be an alias to a string.
definitions:
  jwt:
    type: string

modules:
  # Now we say how to use it. Each key inside `modules` is the name of an
  # entrypoint within your Elm app. Here we're saying that this module is named
  # `Main`, which means we'll be able to access it in TypeScript at `Elm.Main`.
  Main:
    # Inside the app, we specify that you have to start the app by providing
    # the current value. We say that it's nullable because we don't know if the
    # user is logged in at this point.
    flags:
      properties:
        currentJwt:
          ref: jwt
          nullable: true

    # Next, we set up the port for Elm to tell JavaScript that it should store
    # a new JWT. Unlike flags, ports have a direction. We specify that we're
    # passing a message from Elm to JavaScript with `metadata.direction`.
    ports:
      newJwt:
        metadata:
          direction: ElmToJs
        ref: jwt

You get this TypeScript from that:

// Warning: this file is automatically generated. Don't edit by hand!

declare module Elm {
  namespace Main {
    type Flags = {
      currentJwt: string | null;
    };

    type Ports = {
      newJwt?: {
        subscribe: (callback: (value: string) => void) => void;
      };
    };

    function init(config: { flags: Flags; node: HTMLElement }): {
      ports?: Ports;
    };
  }
}

and this Elm:

module Main.Flags exposing (..)

{-| Warning: this file is automatically generated. Don't edit by hand!
-}

import Dict exposing (Dict)
import Json.Decode
import Json.Decode.Pipeline
import Json.Encode


type alias Flags =
    { currentJwt : Maybe String
    }


flagsDecoder : Json.Decode.Decoder Flags
flagsDecoder =
    Json.Decode.succeed Flags
        |> Json.Decode.Pipeline.required "currentJwt" (Json.Decode.nullable Json.Decode.string)


encodeFlags : Flags -> Json.Encode.Value
encodeFlags flags_ =
    Json.Encode.object
        [ ( "currentJwt"
          , case flags_.currentJwt of
                Just value ->
                    Json.Encode.string value

                Nothing ->
                    Json.Encode.null
          )
        ]

These types can be incrementally adopted, of course, so I hope you’ll check it out and let me know how you get along. Just to put that link again on the other side of these big code blocks, you can get the project at GitHub - BrianHicks/elm-duet.

Thanks, and happy Elming! :tada:


  1. Definitely worth acknowledging Dillon Kearns’ elm-ts-interop, which can do similar things. Nothing wrong with that library, but I wanted to explore if JTD could be a good fit here, and I think it is! ↩︎

22 Likes

Awesome! Since I see it’s written in Rust, may I ask, do you intend to add Rust in the language list? Our backend is Rust which generates Elm, I wonder if we could replace it with elm-duet and get all three in a trio? :slight_smile:

I don’t intend to in the short term, but it’d be nice to break the ports/flags/types apart in the long term to make a more generic JTD generator.

Perhaps this could help, since duet requires you to write the JTD manually anyway, you could then use it with Rust:

1 Like