Purescript-marked - Statically checked bindings to marked.js

I am happy to announce purescript-marked, a library that provides PureScript bindings to marked.js.

From the perspective of a library user there’s nothing very special about this library. It provides one module with low level bindings to marked.js and another one with a more idiomatic PureScript wrapper around it.

However, implementation wise it may be interesting for people who are writing FFI code in general. Some weeks ago I wrote a blog post about how to make FFI safer, in particular by using the ts-bridge library. This is the first project where I am consequently applying this approach in a larger real world context.

This means that TypeScript types are generated out of the PureScript types from the library’s bindings module. Since TS types already exist for marked.js, the TypeScript compiler can be used to check the correctness of the generated types. This check for instance runs in CI.

Admittedly it would be better if it worked the other way around: PureScript types would be generated out of the TypeScript types. But a tool like that doesn’t exist yet. With ts-bridge we still have to manually write the bindings, but we can verify if they are correct. Which is, I’d say, already a big win.

The marked.js library is a good example of how JavaScript API’s look in the wild:

  • Union types with flat encodings.

    { type: "caseOne", foo: string } |
    { type: "caseTwo", bar: number, baz: boolean }
    
  • Record fields which can be null (and/or undefined) on top of the fact that they are optional.
    { foo?: number | null }

  • Unions of string literals
    "left" | "right" | "center"

  • Intersection types:
    Array<number> & { foo: string } (yes, this is a thing!)

And additionally, the main type from the library is recursive. All of this is expressed with PureScript types in the bindings module. PureScript builtin types as well as helper types like VariantEncodedFlat, OneOf or TsRecord are used to project the TypeScript types into PureScript.

In the bindings module no runtime conversion is done. That means consuming this “low level” API does not introduce any performance overhead. The code in the idiomatic wrapper module converts the utility types from the bindings module into more idiomatic PureScript types. E.g. untagged unions are converted into sum types, optional fields are converted into Maybe, etc. Writing this conversion is finally only boilerplate code and for large parts a very type safe process.

I generated some tests and you can guess what was the outcome of the first run: A parsing error at a place where it should not occur. It tuned out that this was caused by a flaw in the upstream TypeScript types: string was used instead of null | string. After I fixed this, all tests passed. marked.js is written in JavaScript and TS types are provided by the community. So there is no guarantee that the types are correct. But this risk is reduced when library code is written directly in TypeScript, which is more and more often the case.

7 Likes

Tried ts-bridge in my three.js FFI project, it caught one typo error already. I do have one question.

The ts-bridge doesn’t support the PS Foreign type. Is it a deliberate design choice? If so, why? I asked because I have a class of bindings in that project that need to deal with heterogeneous js arrays and I model them as Array Foreign.

@edwardw Sorry I am just reading this. No, that was not a design decision. I think we could support Foreign which would correspond to any on the TS side. If you open up an issue on GH, this will be done soon.