0.0.2 • Published 3 years ago

elm-typescript v0.0.2

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

elm-typescript

Type safety from elm to typescript and back.

Caveat: This project is very alpha.

Installation

npm install elm-typescript

Usage

If you have an existing elm project simply run:

npx elm-typescript init

It assumes the paths src/elm and src/ts exists, and that src/elm is configured in elm.json. If this isn't the case you can modify the generated config elm-typescript.json with your paths.

To generate new types after you've modified the config:

npx elm-typescript

Further down you'll find instructions how to set up a project from scratch with elm-typescript.

Config

The config consist of two main parts. A types section and a ports section.

In the types section you can define records. enums and unions.

Records

What you define in the records section is turned into Elm records, and TS interfaces. As an example, to represent an user, you would add something like:

{
  "types": {
    "records": {
      "User": {
        "uid": "Int",
        "name": "String",
        "role": "Role"
      }
    }
  }
}

The field values can for now be one of String, Int, Bool, Unit, ‘List SomeType’, ‘Dict SomeType’ and any of the records, enums or unions you define yourself.

Dicts always use Strings for keys. I could have made the config require you to say Dict String SomeType but went for the shorter version.

Enums

Enums are turned into Elm custom types and TS enums.

{
  "types": {
    "enums": {
      "Role": [ "Admin", "Regular" ]
    }
  }
}

Unions

Unions are turned into Elm custom types and TS union types.

The reason I added both enums and unions are that TS enums are nicer to use compared to TS union types.

I don’t think it makes sense to keep both the enums and unions sections. So my idea is to make them one, and when I can use TS enums I will, otherwise I’ll use TS union types.

Currently unions are defined like this:

{
  "types": {
    "unions": {
      "Event": {
        "Login": {
          "uid": "Int",
          "timestamp": "Int"
        },
        "Logout": {
          "uid": "Int",
          "timestamp": "Int"
        },
        "Message": {
          "message": "String",
          "timestamp": "Int"
        }
      }
    }
  }
}

I think it was a mistake to use this anonymous record syntax, so my plan is to use the same syntax as record values:

{
  "types": {
    "unions": {
      "Event": {
        "Login": "LoginEvent",
        "Logout": "LogoutEvent",
        "Message": "MessageEvent"
      }
    }
  }
}

And you’ll have to define the records yourself.

Ports

In the ports section you define any incoming and outgoing messages:

{
  "ports": {
    "toElm": {
      "userLoggedIn": "User",
      "eventsFetched": "List Event"
    },
    "fromElm": {
      "logout": "()"
    }
  }
}

Codegen

The types are put in gen/types.ts and 'Gen/Types.elm. In Types.elm` there are also generated encoders and decoders. The only thing missing are encoders for union types. I’ve only used unions to send data to Elm so simply haven’t needed them. But should be easy enough to add.

The ports are put in gen/ports.ts and Gen/Ports.Elm.

TS

To wire up the TS side of things you’ll do something like this:

import Ports from "./gen/ports";
import { User, Role } from "./gen/types";
import { Elm } from "../elm/Main";

const app = Elm.Main.init();

const ports = Ports.init(app, {
  logout: () => {
    // do something to logout the user
  },
});

// when user is logged in

const user: User = {
  uid: 42,
  name: "Smu",
  role: Role.Admin,
};

// the TS type signature enforces only valid `User` types as passed
ports.userLoggedIn(user);

// when events are fetched
// similarly, only a list of `Event` types are allowed
ports.eventsFetched([]);

Oh, I also generate a index.d.ts which is why the import { Elm } from "../elm/Main" bit works

Elm

To wire up the Elm side of things you’d do something like:

import Gen.Ports as Ports

type Msg
    = UserLoggedIn User
    | EventsFetched (List Event)

subscriptions : Model -> Sub Msg
subscriptions _ =
    Ports.subscribe
        { userLoggedIn = UserLoggedIn
        , eventsFetched = EventsFetched
        }

Ports.elm also exposes functions for the outgoing ports ala logout : () -> Cmd msg.

Setting up a elm project from scratch with elm-typescript

To test it out yourself follow these steps:

npm init -y
npm install elm 
npm install galactic-run/elm-typescript
mkdir src src/elm src/ts

Then we’ll need to init elm:

npx elm init

And install a dependency:

npx elm install elm/json

Then edit elm.json and update source-directories from src to src/elm.

Now to generate some code:

npx elm-typescript init

This will create an example config and generate a bunch of code. Have a look at the config in elm-typescript.json and the corresponding code in str/elm/Gen and src/ts/gen.

If you modify the config you simply run the codegen without the init option npx elm-typescript

If you use parcel bundler, which supports Elm and TS out of the box, you should be able to get going by adding a 'index.html, some TS to wire things up and a Main.elm.

I plan on adding a simplistic working example, and perhaps write some of this brain dump into a more structured blog post or something soon.