.github | ||
examples | ||
scripts | ||
src | ||
tests | ||
.envrc | ||
.gitignore | ||
Cargo.lock | ||
Cargo.toml | ||
elm.ts | ||
flake.lock | ||
flake.nix | ||
ideal.d.ts | ||
README.md |
elm-duet
Elm is great, and TypeScript is great, but the flags and ports between them are hard to use safely. They're the only part of the a system between those two languages that aren't typed by default.
You can get around this in various ways, of course, either by maintaining a definitions by hand or generating one side from the other. In general, though, you run into a couple different issues:
- It's easy for one side or the other to get out of date and errors to slip through CI and code review into production.
- Definitions in one language may not be translatable to the other (despite the two type systems having pretty good overlap.)
elm-duet
tries to get around this by creating a single source of truth to generate both TypeScript definitions and Elm types with decoders.
We use JSON Type Definitions (JTD, five-minute tutorial) to say precisely what we want and generate ergonomic types on both sides (plus helpers like encoders to make testing easy!)
Here's an example for an app that stores a jwt in localStorage
or similar to present to Elm:
{
"modules": {
"Main": {
"flags": {
"properties": {
"currentJwt": {
"type": "string",
"nullable": true
}
}
},
"ports": {
"newJwt": {
"metadata": {
"direction": "ElmToJs"
},
"type": "string"
},
"logout": {
"metadata": {
"direction": "ElmToJs"
}
}
}
}
}
}
You can generate code from this by calling elm-duet path/to/your/schema.json
:
$ elm-duet examples/jwt_schema.json --typescript-dest examples/jwt_schema.ts --elm-dest examples/jwt_schema
wrote examples/jwt_schema.ts
wrote examples/jwt_schema/Main/Flags.elm
wrote examples/jwt_schema/Main/Ports.elm
Which results in this schema:
// Warning: this file is automatically generated. Don't edit by hand!
declare module Elm {
namespace Main {
type Flags = {
currentJwt: string | null;
}
type Ports = {
logout: {
subscribe: (callback: (value: Record<string, never>) => void) => void;
};
newJwt: {
subscribe: (callback: (value: string) => void) => void;
};
}
function init(config: {
flags: Flags;
node: HTMLElement;
}): void
}
}
And these Elm flags:
module Main.Flags exposing (..)
{-| Warning: this file is automatically generated. Don't edit by hand!
-}
type alias Flags =
{ currentJwt : Maybe String
}
flagsDecoder : Decoder Flags
flagsDecoder =
Decode.map Flags
(Decode.field "currentJwt" (Decode.nullable Decode.string))
encodeFlags : Flags -> Encode.Value
encodeFlags flags =
Encode.object
[ ( "currentJwt"
, case flags.currentJwt of
Just value ->
Encode.string value
Nothing ->
Encode.null
)
]
And these for the ports:
module Main.Ports exposing (..)
{-| Warning: this file is automatically generated. Don't edit by hand!
-}
type alias Logout =
()
logoutDecoder : Decoder Logout
logoutDecoder =
Decode.null ()
encodeLogout : Logout -> Encode.Value
encodeLogout logout =
Encode.null
type alias NewJwt =
String
newJwtDecoder : Decoder NewJwt
newJwtDecoder =
Decode.string
encodeNewJwt : NewJwt -> Encode.Value
encodeNewJwt newJwt =
Encode.string newJwt
port logout : Value -> Cmd msg
logout_ : Logout -> Cmd msg
logout_ value =
Debug.todo "send"
port newJwt : Value -> Cmd msg
newJwt_ : NewJwt -> Cmd msg
newJwt_ value =
Debug.todo "send"
Here's the full help to give you an idea of what you can do with the tool:
$ elm-duet --help
Generate Elm and TypeScript types from a single shared definition.
Usage: elm-duet [OPTIONS] <SOURCE>
Arguments:
<SOURCE> Location of the definition file
Options:
--typescript-dest <TYPESCRIPT_DEST> Destination for TypeScript types [default: elm.ts]
--elm-dest <ELM_DEST> Destination for Elm types [default: src/]
-h, --help Print help
-V, --version Print version