Community Edition Setup Guide

Install the CLI#

npm install --save-dev elm-ts-interop

Run the init Command#

npx elm-ts-interop init

This will give you two files, InteropPorts.elm and InteropDefinitions.elm.

  • InteropDefinitions.elm is where you maintain your type-safe elm-ts-json Encoders and Decoders. This is the source of truth for the generated TypeScript types. This generated file is just a starting point for you to build off of.
  • InteropPorts.elm is a file to help you safely use your elm-ts-interop ports and flags. You probably don't want to modify it, but do keep it in your version control system.

Choose a filename for your generated TypeScript Declarations#

elm-ts-interop --output <FILE-NAME-FROM-INSTRUCTIONS-BELOW>

Figure out the style of your imports you use to pull Elm in from JavaScript/TypeScript.

  1. No .elm extension (Webpack usually does this style) import { Elm } from "./src/MyMainModule" or require("./src/MyMainModule")
  2. With .elm extension (ViteJS usually does this style) import { Elm } from "./src/MyMainModule.elm" or require("./src/MyMainModule.elm")
  3. HTML script tag <script src="/path/to/compiled/elm.js">

Based on the import style, use the corresponding setup:

  1. elm-ts-interop --output src/MyMainModule/index.d.ts. The types should be automatically picked up from your import/require statement.
  2. elm-ts-interop --output src/MyMainModule.elm.d.ts. The types should be automatically picked up from your import/require statement.
  3. You can generate the file anywhere. For example, elm-ts-interop --output elm.d.ts. Then use a Triple Slash Reference Directive Comment at the very top (must be first line) of your TypeScript or JavaScript file to include the generated types as global types /// <reference path="./elm.d.ts" />

Whichever style you use, add the generated .d.ts file to your .gitignore file. That way you can be confident that your builds are using a fresh copy, not a stale one from version control that hasn't been re-generated.

Using Type-Safe Flags and Ports#

  • Use InteropPorts.decodeFlags to decode your flags
  • Create Cmd msg ports by passing InteropDefinitions.ToElm variants to InteropPorts.toElm
  • Set up subscriptions with InteropPorts.fromElm
  • InteropDefinitions.elm is user-maintained. Change it any time you want to add/remove/change ports or flags.

For example, to send our Alert port, we can use this Cmd in our Elm app's init.

greet : Cmd msg
greet =
    "Hello from elm-ts-interop!"
        |> InteropDefinitions.Alert
        |> InteropPorts.fromElm

Using Flags from JavaScript/TypeScript#

elm-ts-interop uses a single port pair: interopFromElm and interopToElm. Other than the type information in your flags and the port pair, the wiring is like any other Elm app.

You will typically have a union type for your interopFromElm, which you can use a switch statement to match and get typed data for each branch. This is convenient because you can do an exhaustiveness check to make sure that you've handled every message in interopFromElm.

Note: TypeScript doesn't have errors for non-exhaustive switch statements, but you can add an eslint rule, @typescript-eslint/switch-exhaustiveness-check and configure it as an error like the elm-ts-interop-starter repo does to make sure you've handled every case.

document.addEventListener("DOMContentLoaded", function () {
  const app = Elm.Main.init({
    node: document.querySelector("main"),
    flags: {
      os: "Windows", // flags are added as normal, just with types to guide you
    },
  });
  app.ports.interopFromElm.subscribe((fromElm) => {
    switch (fromElm.tag) {
      case "alert": {
        alert(fromElm.data.message);
        break;
      }
    }
  });

  myAuthenticationService.onAuthenticated((user) => {
    app.ports.interopToElm.send({
      tag: "authenticatedUser",
      username: user.username,
    });
  });
});