Fork me on GitHub
#clojurescript
<
2024-04-11
>
Yehonathan Sharvit11:04:55

I would like to make a ClojureScript library consommable by a TypeScript client. Any advice and tips?

thheller11:04:55

with :js-provider :external when building as library

Yehonathan Sharvit11:04:14

I have an additional concern: My Cljs lib expects keyword values inside some field maps e.g.

{:name "Joe"
 :role :admin}
How this map could be passed in JS/TS?

Yehonathan Sharvit11:04:40

I am talking about the field value :admin.

p-himik11:04:15

You can create and export vars that JS can use, so it might end up something like this:

the.app.namespace.create_the_map("Joe", the.app.namespace.admin_role)

Yehonathan Sharvit11:04:59

Interesting. It can simply be:

{name: "Joe",
 role: the.app.namespace.admin_role}

Yehonathan Sharvit11:04:05

Or even:

{name: "Joe",
 role: cljs.core.keyword("admin")}

Yehonathan Sharvit11:04:00

What do you guys think?

p-himik12:04:13

cljs.core.keyword is not exported, it will not be available after advanced optimizations. Also, that's not a CLJS map, so it'd have to be converted to one by something.

Yehonathan Sharvit14:04:49

My plan is to use js->clj in the exported function.

p-himik14:04:11

Then that very function can simply call keyword on :role.

Yehonathan Sharvit14:04:42

Right. But I am looking for a generic way to deal with it

p-himik14:04:32

Requiring your users to call keyword on their data is shifting the responsibility from your code to theirs. It does make your code more generic, but makes their code less generic.

Yehonathan Sharvit14:04:51

I realise it now. What can I do then?

thheller14:04:59

kinda hard to give advice without knowing what the actual problem is

thheller14:04:10

there is clj->js and js->clj if want to convert data

p-himik15:04:44

> What can I do then? I would make the API convenient to users and do all the necessary conversions on the CLJS side. You can do it manually, you can do it via some spec/schema library's coercion.

Ed17:04:20

In something like typescript, people are going to expect static types for things like roles. Be it an enum or an object exported as const, I'd suggest writing a typescript shim for your api that does the conversions and calls into the cljs code. I think that's the safest way of doing these kind of conversions.

p-himik17:04:09

IMO a shim that does additional work and calls into CLJS is more work than just the result of CLJS compilation + TS type definitions.

Yehonathan Sharvit12:04:21

Another question: how to define the types for TypeScript for the functions my CLJS library exposes?

p-himik12:04:22

The same exact way you'd do it for a JS library that doesn't define any types in specially formatted comments - by manually writing a .d.ts file with all the relevant types in it. From the example in the docs thheller has linked above - you have a /js/demo.js file that exports { hello }. Then you'd add /js/demo.d.ts that has export function hello (): void;.

Yehonathan Sharvit12:04:38

In the docs, /js/demo.js is a generated file. Can shadow-cljs bundle ts source files?

p-himik12:04:42

It can be done via a build hook. You'd have the TS file in your sources and the hook would copy it to the output dir.

thheller13:04:16

shadow-cljs cannot bundle ts files no, but you can run tsc manually and have that produce esm or commonjs files. shadow-cljs can bundle those.

Yehonathan Sharvit11:04:50

Do you think type information is preserved when producing esm/commonjs files?

thheller11:04:22

I don't know what that means or what it would be relevant for

Yehonathan Sharvit11:04:41

I want a TypeScript consumer to see the types that I declare.

p-himik11:04:31

Just tested. TL;DR: you should just stick to creating .d.ts files manually. It's better in almost every way. Docstrings are turned into comments, but those are removed with any level of optimization, including :whitespace. So you have to use :none, but that doesn't work for release builds. So you have to use a development build and then somehow stitch together all the separate build artifacts. defs in CLJS become members of a global object in JS, so all you can document is the type of an object member. If that member is a function, you have to document it as such using JSDoc in its docstring - "this is a function that returns this and accepts this", which can be cumbersome (although I might be wrong here - maybe TS understands when a member is documented as a function). It won't work at all for functions with multiple arities because those become separate members with some generated entry point.

p-himik11:04:00

Oh! And docstrings are only at the impl level. The interface file that has the exports doesn't have the comments at all. No clue whether TS will follow any references during compilation. I'd assume that it wouldn't.

Yehonathan Sharvit11:04:15

Thanks a lot!

👍 1
Vladimir Pouzanov19:04:52

I'm slightly confused about arrays. Why is this not an array? (array? (type (js->clj #js["a"]))) => false

1
Vladimir Pouzanov19:04:58

derp, never mind. it's a vector.

Vladimir Pouzanov19:04:05

it was a long day 🙂

p-himik19:04:17

Also, array? checks not types but values.

dpsutton20:04:13

❯ clj -A:cljs -M -m cljs.main -re node -r
ClojureScript 1.10.773
cljs.user=> (type (js->clj #js["a"]))
cljs.core/PersistentVector
cljs.user=> (type *1)
#object[Function]
cljs.user=> (array? (type (js->clj #js["a"])))
false
cljs.user=> (array? #js["a"])
true
cljs.user=> (array? (js->clj #js["a"]))
false
cljs.user=>

👍 2