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!
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! ↩︎