Elm Tigershark
Enjoy type-safe interop between Elm and TypeScript with automatically typed flags and ports.
This tool is meant as a mostly drop-in replacement for
elm-typescript-interop
,
so if you don't know what's up start there. While elm-typescript-interop
supports Elm 0.18 well but has known bugs with Elm 0.19, elm-tigershark
only
supports Elm 0.19.
Installation
Tigershark is an npm package and can be installed via npm i elm-tigershark
or
similar commands.
Usage
Unlike elm-typescript-interop
, tigershark uses CLI arguments similar to the
Elm compiler in order to specify the Elm input files and generated output file.
This level of control allows tigershark to support some interop use cases that
are not possible with elm-typescript-interop
.
A single Elm program
For a project with one Elm program, we might compile our Elm code and generate type declarations like this:
elm make src/Main.elm --output=src/elm-main.js
tigershark src/Main.elm --output=src/elm-main.d.ts
Importantly, the compiled javascript file and type declaration file have the
same name so when we use an import directive, such as import {Elm} from 'elm-main'
, TypeScript is able to match up the generated types to the compiled
Elm code. We could instead use an index file, in this case meaning
--output=src/elm-main/index.d.ts
Multiple Elm programs
For a project with multiple Elm programs, we may choose to compile each program
separately, like above, or compile multiple Elm programs together into one
javascript output. In this later case we need to specify the same files when
calling elm make
and tigershark
so that the output includes types for each
Elm program:
elm make src/*.elm --output=src/elm-programs.js
tigershark src/*.elm --output=src/elm-programs.d.ts
Once again, the .d.ts
type declaration file name should match the file name of
the compiled javascript asset. Like elm make
, tigershark will identify which
of the provided Elm input files contains a program main function and produces a
type for each program.
Webpack and other asset loaders
If we're using Elm with elm-webpack-loader
or some other asset management
system, then we will have to take advantage of TypeScript's wildcard module
declarations
feature to associate types with imported Elm files. This use case has some
unfortunate nuance, so we'll step through it carefully.
When using elm-webpack-loader
the build system identifies which Elm files to
find and compile based off of javascript imports with the .elm
extension.
Given two Elm programs, Foo.elm
and Bar.elm
, we might import and use the
programs like this:
import { Elm as ElmFooApp } from "Foo.elm";
import { Elm as ElmBarApp } from "Bar.elm";
...
ElmFooApp.Foo.init(fooNode, fooFlags)
ElmFooApp.Bar.init(barNode, barFlags)
In previous examples we noted that Typescript pairs untyped javascript files to
type declaration files based on file name. How do we accomplish this pairing when
the file name has an extension? Unfortunately we can't. The only way to
associate types to our Elm imports is by wrapping the type declarations in a
wildcard module, specifically declare module '*.elm'
. This directive tells
TypeScript that every file imported with a .elm
extension will use the
module's type, regardless of file name.
Here's how we might generate types for our Elm and Typescript project that uses webpack to manage Elm compilation:
tigershark src/Foo.elm src/Bar.elm --output=src/elm.d.ts --tsModule='*.elm'
The name of the output file is no longer relevant, since it's superseded by the
wildcard module declaration. The tsModule
argument can be changed to support
different asset loaders which might use a different import syntax.
Finally, elm-tigershark
comes with a webpack plugin to automatically
regenerate type declarations when Elm files are changed. The plugin API has not
yet been finalized.
Roadmap to 1.0.0
- Fix detection of indirect port use
- Basic error messages
- Basic CLI help output
- Improve ElmTigersharkWebpackPlugin API
Known issues
- The current method of finding ports associated to an Elm program is flawed and needs to be rewritten. If you declare a port in a file, but then wrap the port in a different function and export the wrapping function, the indirect use of the port will not be detected.
- In Elm 0.19 the
--optimize
flag removes unused ports from the compiled javascript output. These unused ports are still included in the generated type definition, but calling the port functions from javascript produces a runtime error.
Future enhancements
- Auto generate TS types, encoders, and decoders for message passing ports.
- On init, preemptively read files that we know will be needed.
- Make read file requests for multiple files at once.
- Write custom "isPortModule" parser so that we don't have to fully parse a
file with
elm-syntax
to find out if it's declared as aport module
. - Cache ASTs in the
Project
so that files don't have to be repeatedly re-parsed.
The name
This isn't a useful tool yet so it doesn't have a useful tool name. TypeScript and Tigershark sound a bit similar. That's all I've got.