Elm-ts-interop-pro Demo and Setup Tutorial

I made a video showing the elm-ts-interop-pro CLI in action, including a setup from scratch. Check it out - I even show how to send/receive custom types through ports, which was the big motivation of building this tool in the first place :grinning_face_with_smiling_eyes:

We also did an Elm Radio episode about elm-ts-interop, and the landing page is at https://elm-ts-interop.com.

The tl;dr version is that the dillonkearns/elm-ts-json Elm package mostly a drop-in replacement for elm/json JSON Decoders and Encoders, but with TypeScript type information. The CLI gets that type information from the decoders to keep your ports and flags in sync with your Elm code.

12 Likes

Sorry, I’m missing something.

There’s mention of a community version to try (instead of using @incrementalelm/elm-ts-interop-pro npm package). And seems like it’s supposed to be elm-ts-interop

$ npm install --save elm-ts-interop

added 35 packages, and audited 36 packages in 2s

1 package is looking for funding
  run `npm fund` for details

found 0 vulnerabilities
$ elm-ts-interop                   
node:internal/modules/cjs/loader:928
  throw err;
  ^

Error: Cannot find module './rewrite-elm-json.js'
Require stack:
- /Users/choonkeat/git/try-elm-ts-interop/node_modules/elm-ts-interop/src/index.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:925:15)
    at Function.Module._load (node:internal/modules/cjs/loader:769:27)
    at Module.require (node:internal/modules/cjs/loader:997:19)
    at require (node:internal/modules/cjs/helpers:92:18)
    at Object.<anonymous> (/Users/choonkeat/git/try-elm-ts-interop/node_modules/elm-ts-interop/src/index.js:11:24)
    at Module._compile (node:internal/modules/cjs/loader:1108:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
    at Module.load (node:internal/modules/cjs/loader:973:32)
    at Function.Module._load (node:internal/modules/cjs/loader:813:14)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/Users/choonkeat/git/try-elm-ts-interop/node_modules/elm-ts-interop/src/index.js'
  ]
}

Here’s some debug

$ which elm-ts-interop
./node_modules/.bin/elm-ts-interop

$ tree node_modules/elm-ts-interop 
node_modules/elm-ts-interop
├── package.json
└── src
    └── index.js

1 directory, 2 files

I tried looking for https://github.com/dillonkearns/elm-ts-interop but that repo just redirects me to GitHub - dillonkearns/elm-ts-json which doesn’t seem to be a node cli (just the elm package) because my node_modules/elm-ts-interop/src/index.js doesn’t seem to be elm-ts-json/index.js at a75e6207f6f0af8d692c2eab15cb1d5d6fc6d46a · dillonkearns/elm-ts-json · GitHub

Hey @choonkeat, thanks for letting me know! I pushed up a fix, it should work if you bump to the latest elm-ts-interop NPM package now.

I’m still trying to figure out the best way to manage the pro and community editions, because I maintain them in a monorepo, so it’s difficult to manage the distribution mechanism between those two. I don’t have a repo with the community edition CLI right now. I might set up a mirror at some point to sync with the mono repo.

I’ll definitely be getting some more docs and materials for setting up and running the community edition soon! Hope you enjoy it once you get it up and running!

1 Like

Thanks for the quick fix. My initial trial was bit bumpy. You might want to consider some default files

  1. src/InteropDefinitions.elm

    module InteropDefinitions exposing (..)
    
    import TsJson.Decode
    import TsJson.Encode
    
    interop =
        { flags = TsJson.Decode.bool     -- these example values
        , toElm = TsJson.Decode.int      -- are likely incorrect
        , fromElm = TsJson.Encode.string -- but, it's a start
        }
    
  2. tsconfig.json

    {
        "compilerOptions": {
            "allowJs": true, /* Allow javascript files to be compiled. */
            "checkJs": true, /* Report errors in .js files. */
            "strict": true /* Enable all strict type-checking options. */
        }
    }
    
  3. maybe a dependency on @types/node (optional)

Also, the generated .d.ts file a line export { Elm }; causing this warning/error

'Elm' refers to a UMD global, but the current file is a module. Consider adding an import instead.ts(2686)

which I’m not sure what I should do

The output from elm make uses UMD, which means it supports both CommonJS or being added to the global namespace. TypeScript will give a warning about using UMD unless you set this in your tsconfig.json:

"allowUmdGlobalAccess": true

They give a little more detail in the docs for allowUmdGlobalAccess.

Yeah, that’s a good idea. If the CLI is run and the definitions module doesn’t exist then it could offer to create a sample file. Thanks for the feedback. Typically the toElm and fromElm values will be a custom type, so that might be a good starting point for most cases.

I’m not sure I’m following why a dependency on @types/node would be helpful. If you’re not using it in the context of a node application, then it would give you incorrect type information wouldn’t it?

thanks for the link!

ah sorry my head is elsewhere. you’re right :slight_smile:

Detected problems in 1 module.
-- MODULE NOT FOUND ------------------------------------------ CodeGenTarget.elm

You are trying to import a `InteropDefinitions` module:

3| import InteropDefinitions
          ^^^^^^^^^^^^^^^^^^

Sorry, one last question: is there a way I can customise this file (or module name) in the cli? Reason is that I have multiple entry points and would like a definition file for each Client.elm and Server.elm e.g. as src/Client/Interop.elm and src/Server/Interop.elm respectively

Good idea, yeah I’ll add that :+1:There’s an option for that for the pro CLI, I’ll add it to the community edition as well.

1 Like

oh, wasn’t aware. might need a community vs pro comparison table. or having options all show up in --help but let it be known what’s only available for pro

1 Like

I’m having trouble getting it to apply the rules in VSCode. My setup is slightly different, but I’m not sure which part of the difference is causing it.

It seems like with your setup it’s just automatically inferring the right types just by including the types file.

Here’s what part of mine looks like (I’m using Parcel V1 for my app generally):

import { Elm } from '../elm/Site.elm'
import { ElmApp, FromElm } from './utils/definitions/site'

const targetDiv = document.getElementById('elm-site')

const app: ElmApp = Elm.Site.init({
  node: targetDiv,
  flags: {
    current_time: new Date().getTime()
  }
})

app.ports.interopFromElm.subscribe((fromElm: FromElm) => {
  switch (fromElm.tag) {
    case "reportError":
      Errors.report(fromElm.data)
      break
  }
})

For a while TypeScript didn’t like the .elm import, but used this trick to get it to allow that. I’m still new-ish to TypeScript so I may be missing obvious things.

And tsconfig.json (I’m new at using tsconfig as well):

{
  "compilerOptions": {
    "strict": true,
    "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "allowJs": true,
    "checkJs": true,
    "allowUmdGlobalAccess": true,
    // "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "noEmit": true,
    "lib": [
      "dom",
      "es7"
    ],
    "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true, /* Enable strict null checks. */
    "strictFunctionTypes": true, /* Enable strict checking of function types. */
    "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
    "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
    "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "esModuleInterop": false, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "skipLibCheck": true, /* Skip type checking of declaration files. */
  },
  "include": [
    "ts/**/*"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}

As far as I could tell, including a line like /// <reference path="./src/Main/index.d.ts" /> from your example didn’t have an impact on the type detection.

Any idea why it might not be applying the rules with this setup? Specifying fromElm: FromElm for the ports does work for that part, but I’m not sure what to do to specify the rest, especially the Elm app initialization part.

It looks like your entrypoint module is Site.elm? Are you specifying the entrypoint when you run elm-ts-interop with --entrypoint Site?

If you use --entrypoint Main --output elm/Site/index.d.ts then TypeScript should even pick up those types automatically from your import.

Let me know how that goes!

The command I’ve been running for for the Elm app at elm/Site.elm is

elm-ts-interop-pro --gen-directory=elm --definitions=elm/Site/Definitions.elm --gen-module=Site.InteropPorts --entrypoint Site --output ts/utils/definitions/site.d.ts

Same result with

elm-ts-interop-pro --gen-directory=elm --definitions=elm/Site/Definitions.elm --gen-module=Site.InteropPorts --entrypoint Site --output elm/Site/index.d.ts

My main elm file isn’t a Main.elm file because I’ve got a few different Elm apps getting compiled for different pages on the project, so for this particular elm app it’s at elm/Site.elm.

Are the file names (e.g. index.d.ts vs site.d.ts) or locations (e.g. index.d.ts being in the elm folder structure and/or positioned relative to the Site.elm file) important?

Yes, the index.d.ts and file location is important. The idea is that when you do an import like

import { Elm } from '../elm/Site'

TypeScript will resolve the location for both a js file (which something like Webpack will transform that underlying Elm code into), and a TypeScript declaration file (index.d.ts). If it wasn’t under the same starting path but with index.d.ts at the end, then TypeScript wouldn’t be able to resolve it.

I noticed that your import is using import { Elm } from '../elm/Site.elm', so that may be what’s causing the problem. I think that will prevent TypeScript from picking up the index.d.ts there.

I wonder if it would work if you used --output elm/Site.elm.d.ts.

This page has more in-depth documentation about how TypeScript looks up type information: https://www.typescriptlang.org/docs/handbook/module-resolution.html. It’s also possible to create an explicit mapping using paths in your tsconfig.json, but it’s ideal if you can avoid that and get it to resolve types without that.

Ah! Good catch! I changed the import to just

import '../elm/Site.elm'

and took out the explicit import of the index.d.ts file and that worked.

Seeing a warning over Elm.Site.init: 'Elm' refers to a UMD global, but the current file is a module. Consider adding an import instead.

But it’s all working! Thank you!

1 Like

Awesome! :tada:

For the UMD warning, check out this solution: Elm-ts-interop-pro Demo and Setup Tutorial - #5 by dillonkearns.

Ah! After some fiddling: I had that setting set already, but needed to switch to import { Elm } from '../elm/Site' to get it to close the loop I guess (also it actually wasn’t successfully compiling with import '../elm/Site.elm' — I just hadn’t noticed because I was distracted by the types working). It is now both seeing the types and successfully compiling. Takeaways for me and possibly others:

  • index.d.ts must be named index and must be at elm_source_folder/ElmAppName/index.d.ts (src/Main/index.d.ts for a standard elm app)
  • DO NOT put the .elm extension on the Elm app import statement
  • DO NOT explicitly import the types file separately from the Elm app itself
1 Like

Also fun fact: the unsimplified version of my Elm initialization actually looks like this:

  const app = Elm.Site.init({
    node: targetDiv,
    flags: {
      current_time: new Date().getTime(),
      graphql_flags: graphqlFlags
    }
  })

I’m generating my flags that come from the back-end with graphql and using elm-graphql to decode them so they’re type-safe all the way from the back-end. Hoping to do a write-up at some point, but I thought you might think that was cool.

1 Like

Wow, that’s very cool! Nice idea, that makes a lot of sense as a way to bootstrap some initial data.

1 Like