spotifyovercastrssapple-podcasts

elm-ts-interop

elm-ts-interop is a tool that keeps types in sync between your Elm ports and JavaScript (or TypeScript) wiring code. We talk about the new design and compare it to the original.
March 1, 2021
#25

What's the source of truth?

Getting Started

  • Get your discount code, and learn more about the Pro version, including the scaffolding tool and Pro CLI at elm-ts-interop.com.

Two articles about the redesign of elm-ts-interop (originally published in Bekk's Functional Christmas posts)

Transcript

[00:00:00]
Hello Jeroen! Hello Dillon! Today we're going to talk about a new project of yours. Yes we are,
[00:00:06]
it's kind of a big day, it's kind of been... things have been leading up to this day for a while,
[00:00:11]
I've been sort of working without announcing things publicly too much, and on the day you're
[00:00:18]
listening to this episode, it's the big announcement day. Although actually when we're recording this,
[00:00:23]
of course, it's not quite there yet. Yeah, and Dillon's gonna stress out for a few weeks before
[00:00:28]
the big release. I've got... it's always the 90 90 rule, the first 90% of the project takes the
[00:00:38]
first 90% of the time, and the other 90% of the project takes the other 90% of the time.
[00:00:46]
So today, like I'm making big quotes with my fingers here, Dillon is releasing Elm TS Interop.
[00:00:54]
That's right. So what is Elm TS Interop? What is Elm TS Interop? Good question. So Elm TS Interop,
[00:01:02]
in a nutshell, the goal of the project is to allow you to wire up your Elm application. So when you
[00:01:09]
say elm.main.init and then you take that resulting value app and you say app.ports.hello.subscribe
[00:01:20]
or whatever it may be, the goal of Elm TS Interop is to give you TypeScript type information about
[00:01:27]
what data will flow in and out of those ports. So in a nutshell, that's what Elm TS Interop does.
[00:01:33]
Now, some listeners may be familiar with the previous project of mine that was called Elm
[00:01:38]
TypeScript Interop. Oh, such a different name. Such a different name. I'm rebranding slightly.
[00:01:44]
I don't know if that's more or less confusing. So maybe we can talk a little bit about what Elm
[00:01:51]
TypeScript Interop was, which I'm now deprecating so that we can contrast that with what Elm TS
[00:01:59]
Interop is. Yeah, that sounds good. So Elm TypeScript Interop used static analysis, which,
[00:02:06]
you know, you know your way around Elm static analysis, I'd say. I've dabbled in it.
[00:02:11]
Yes. So Elm TypeScript Interop, the deprecated project, I think it served its purpose and,
[00:02:20]
you know, I know some users enjoyed it, but it had some limitations.
[00:02:25]
Yeah, so Elm TypeScript Interop had the same goal, basically.
[00:02:30]
Yes, that's right. It had the same goal with a different approach. So the new Elm TS Interop
[00:02:37]
actually doesn't use static analysis at all. It does not look at your Elm syntax tree at all.
[00:02:44]
Okay, so Elm TypeScript Interop did. And so what it would do is it would look through your Elm
[00:02:50]
project and it would scan the source code and it would see all of the declared modules,
[00:02:55]
all of the declared ports. So if you say port hello colon string to command message,
[00:03:04]
so that's a port from Elm to JavaScript that sends a value of type string, then you run
[00:03:12]
Elm TypeScript Interop and what it would do is it would find all of those ports. It would find the
[00:03:17]
hello port that you declared. It would see that the type it was sending was a string and it would
[00:03:22]
say, oh great, hello is going to receive a string. So when you say app.ports.hello.subscribe,
[00:03:31]
it's going to take a function which receives a string value. And that works fairly well,
[00:03:37]
but there are some issues with that approach. One problem is, first of all, this goes against
[00:03:44]
some of the recommended best practices for using ports in Elm, right?
[00:03:47]
Yeah. So Murphy Randall had a talk at ElmConf a few years ago where he advised how to use
[00:03:59]
ports and his recommendation was use one output port and one input port for everything.
[00:04:07]
One port pair. Yeah, exactly right. And that's considered a best practice in some ways. There
[00:04:16]
are a few reasons why that tends to work nicely. But one other important thing is that the old Elm
[00:04:23]
TypeScript interop relied a little bit on a hack, which is that you can serialize JSON types through
[00:04:30]
this sort of describing them with Elm types. And then Elm will automatically describe,
[00:04:37]
Elm will automatically say, oh, this port sends a string. This port sends a record with these fields
[00:04:43]
and it will create a sort of serialization thing that turns it into JSON. Evan actually talks about
[00:04:49]
why he doesn't like this sort of implicit serialization in his vision for data interchange,
[00:04:56]
which we'll link to in the show notes. And I mean, basically the idea is that it locks you into this
[00:05:02]
implicit conversion, which is good until it's not. And then when it's not, you can't break out of it.
[00:05:08]
And if something goes wrong, it's hard to tell what went wrong because there's this implicit
[00:05:14]
thing happening. So that's not great. Also, what happens if you're incorrect about the values that
[00:05:20]
you're receiving from JavaScript or TypeScript through a port? Because TypeScript helps you,
[00:05:26]
but it's not perfect in preventing any incorrect type from flowing through.
[00:05:31]
You mean that you call the Elm ports from TypeScript with the wrong values.
[00:05:36]
Exactly. Exactly. With an incorrect flag shape, or there's a field that is non nullable
[00:05:43]
that it's not a maybe in Elm. So you have flags, data is a maybe string. And so that says that
[00:05:52]
it's nullable and it can be null. But if it's a string, not a maybe string and Elm is expecting
[00:05:58]
to receive that value, but it receives a null there, then what happens? It blows up.
[00:06:03]
Wouldn't the TypeScript compiler tell you about that?
[00:06:06]
Well, it would, but you can ignore it. You can also cast types. And in fact,
[00:06:11]
so I wrote a blog post recently about when can we rely on TypeScript. The post is called
[00:06:19]
TypeScript blind spots. And I kind of walk through the difference between the Elm type system and the
[00:06:26]
TypeScript type system and how we can, you can sort of tell TypeScript, trust me, I know what
[00:06:32]
I'm doing here. And it will allow values to flow through that are actually not the types that you
[00:06:37]
said they were, which Elm prevents that through its design. Oh, TypeScript, you naive compiler.
[00:06:46]
Right. And of course it's, I mean, for its goals, its goals are completely different, but it's,
[00:06:51]
you know, it's something to be aware of. Wouldn't these problems still happen with
[00:06:55]
Elm TS interrupt, which we'll go into later, but you still have the same problems because
[00:07:00]
you can still tell TypeScript this is okay, even when it isn't.
[00:07:04]
Right. So yeah, Elm TS interrupt doesn't change the soundness properties of TypeScript type system,
[00:07:10]
but it does not rely on the implicit serialization. Exactly right. Exactly right. So there's this
[00:07:19]
implicit serialization that happens when you say port hello, and then you have a record to
[00:07:25]
command message or whatever, you know, sort of like low level Elm types. There's an implicit
[00:07:32]
serialization that Elm does for you there. And those can fail if TypeScript sends it some values
[00:07:38]
that it's not expecting in an ugly way. So Elm TS interrupt fixes this because it uses an actual
[00:07:47]
JSON decoder encoder. So the only types of data that flow through with Elm TS interrupt are JSON
[00:07:55]
decode values and JSON encode values, which is the recommended practice in the Elm docs.
[00:08:00]
In general, this is the recommended practice because if you send flags that don't
[00:08:07]
correctly do that automatic implicit deserialization, then your app will just have a
[00:08:14]
runtime exception when it's initialized. But you can gracefully handle incorrect assumptions
[00:08:21]
using Elm TS interrupt. Yeah, even for ports not initialization. When you send invalid data to
[00:08:28]
a port from JavaScript to Elm, you get a big red warning in the console. And if you use an encode
[00:08:39]
value, then you can better handle the error. You can handle it correctly in Elm with showing a
[00:08:46]
beautiful error or something. Exactly, right. You can handle that beautifully in Elm. You have
[00:08:52]
access to the fact that something didn't deserialize as expected in your Elm code, not just
[00:08:58]
an error message in your console. So that's one difference. Another difference with Elm TS
[00:09:04]
interrupt is that you have a single port pair like Murphy recommended in his Importance of Ports talk
[00:09:12]
at Elm Conf. So beautiful talk, yeah. Beautiful pun, beautiful pun. We all love a good pun. So
[00:09:18]
it's a single port pair. Now one of the really cool things about that is when you use Elm TS
[00:09:24]
interrupt to handle your ports coming from Elm to TypeScript, then what you can do is you can do
[00:09:31]
a nice beautiful switch statement that looks a lot like your update function in Elm. Yeah, so if the
[00:09:38]
type of this message coming from Elm is increments blah blah blah, then do this, otherwise do that,
[00:09:45]
blah blah blah. Yes, right, exactly. So it's going to look like typically you're going to have a tag
[00:09:52]
which is a string. So in TypeScript there's this idea of a discriminated union type which is the
[00:09:59]
equivalent of what we're used to in an Elm custom type. Sometimes they're called a tagged union. So
[00:10:04]
just to, you know, for anyone who's not familiar with how union types work in TypeScript, they're
[00:10:11]
what are sometimes called untagged union types, which means in TypeScript you can say type string
[00:10:17]
or number equals string pipe number. And now it's valid if you have a value of that type that is
[00:10:26]
a string or a number. Whereas in Elm that's not valid, you need to have a tag that is sort of like
[00:10:32]
a constructor. So you'd have like a, you know, my, you know, my int constructor and you'd have
[00:10:38]
a my string constructor and now you can have a custom type that can be an int or a string,
[00:10:43]
but it has to be sort of contained within this custom type. Well that's what a discriminated
[00:10:50]
union is like in TypeScript. It's just a convention for using a tag. The tag is something
[00:10:56]
that TypeScript has this notion of which is a literal type and literal types are really cool.
[00:11:01]
Literal types are one of my favorite TypeScript features. Are they kind of like enums in other
[00:11:06]
languages? Is that what it is? TypeScript actually has enums also. So you can enum means a lot of
[00:11:15]
different things in different contexts for languages. You know, you can have a Java enum,
[00:11:20]
which is like a particular thing. You can have a C enum, which is just a number. It's just a number
[00:11:27]
under the hood and you don't specify what the number is, but I guess you can, or you can specify
[00:11:32]
the starting value and you can like create one using the enum value's name or a number directly,
[00:11:40]
which is just kind of weird. So it's not exactly that. It is if you said, you know, type host
[00:11:48]
equals string Dillon or string Yeroon. When you say string Yeroon, you say Yeroon between quotes.
[00:11:57]
Exactly. Yeah. Dillon between quotes, pipe, Yeroon between quotes. You can say that in TypeScript
[00:12:04]
and that is literally saying these are the two valid values for this type. It's these strings
[00:12:11]
and the type checker will actually go through your code and check that it's one of those string
[00:12:17]
literals. Whereas with Elm, there's no such concept, right? You could have a custom type,
[00:12:22]
you know, type host equals Dillon pipe Yeroon. And you could have a from string, which is going to
[00:12:30]
convert one of those values from a string into a custom type. But there's no way to annotate
[00:12:39]
a type and constrain the literal values that something can be. So what it is, is it's saying
[00:12:44]
the type is actually a string. Like you pass in a string to create a value of this type. But if you
[00:12:50]
pass in a string, you know, that says George, that's not a valid host. Yeah. You have to prove
[00:12:56]
to the TypeScript compiler that this is either Dillon or Yeroon. Exactly right. Exactly right.
[00:13:02]
So you could create a function that's like, you know, string input to host that's going to take
[00:13:07]
some user input and check if it's one of those valid options and then create one of those
[00:13:11]
constrained literal types. So literal types are a very cool feature of the TypeScript type checker.
[00:13:18]
I mean, it makes sense, right? Because if you think about the history of TypeScript and its goals,
[00:13:23]
you know, it was created to be a way to gradually add types to JavaScript code bases and enhance it
[00:13:29]
incrementally. And so, you know, what do JavaScript code bases do before TypeScript was around?
[00:13:35]
Well, you're going to have a lot of options that are just strings, right? You know, you user role
[00:13:42]
equals admin, user role equals guest. And now, you know, you have those strings flowing throughout
[00:13:49]
your code and you say, if user role equals equals guest, then do this. Yeah. I guess now you can
[00:13:56]
kind of use symbols for that, but. Okay. So one thing about Elm TS interrupt that makes it work
[00:14:03]
really nicely is that TypeScript type system is extremely expressive when it comes to describing
[00:14:10]
JSON values and what goes through ports, JSON values, right? So it works beautifully. Also,
[00:14:19]
JSON values can be serialized. They can be serialized to local storage.
[00:14:23]
JSON values can be serialized and deserialized for HTTP requests, which is actually another area that
[00:14:31]
I'm exploring with the sort of underlying tooling for Elm TS interrupt. But the basic concept is
[00:14:38]
that TypeScript's type system is very expressive at describing JSON values. So like for example,
[00:14:47]
a TypeScript tuple is similar to a literal type. So a literal type can be any type in TypeScript.
[00:14:57]
Like you can even say, you know, you can have a literal JSON object with fields and concrete
[00:15:04]
values, and you can say, you know, this exact type is a valid option here. Oh, and we kind of,
[00:15:11]
to follow up, to close the thread on this discriminated union idea. So the way a
[00:15:16]
discriminated union works in TypeScript is it uses those literal types. So you can say,
[00:15:22]
if you do a discriminated union of type user equals, then you could do like an object. So
[00:15:29]
type user is an object and you could say, you could have a discriminator. So that would just be
[00:15:35]
a property in your, in that object. So you could say, type user equals JSON object notation,
[00:15:45]
kind colon, and then the string guest. And then you can do pipe or, so another variant of user
[00:15:54]
could be object and then kind the same discriminator field. It's just a property in
[00:16:00]
that object, but for the next one, kind colon, and then the string guest and then the string admin,
[00:16:07]
let's say. Yeah. Is kind of anything special or could it be any property in any value?
[00:16:13]
It's just a convention, but the important thing is that now the TypeScript compiler, if you say
[00:16:19]
switch value dot, you know, switch user dot kind. Now it knows based on the kind, which properties
[00:16:28]
are going to exist on that user. So if guest has no properties, admin has an admin ID and a regular
[00:16:35]
user has a first name, last name, whatever. And now it looks very similar to what we do in Elm.
[00:16:41]
Exactly. Now it looks exactly like an Elm custom type in the sense that you know for this tag,
[00:16:48]
these are the values I have for this tag. So it allows you to express that same sort of,
[00:16:56]
it has the same expressive power as an Elm custom type.
[00:17:00]
So from my understanding, the TypeScript type system is more powerful than Elm's and can do
[00:17:07]
anything that the Elm type system allows you to and more, but it's just that it's less sound
[00:17:12]
because there's a lot of uncertainty in the JavaScript world.
[00:17:17]
Yeah, I think that's an accurate description. So just to reiterate what you just said,
[00:17:23]
the TypeScript type system has strictly more features, but fewer guarantees that the types
[00:17:31]
you have are actually going to be correct information so that it lacks that soundness
[00:17:36]
because the way JavaScript works is dynamic. You can cast things and you can fool the type system,
[00:17:43]
whereas in Elm, you have to prove everything to Elm. And so it's sound in that you can't just say
[00:17:50]
this JSON value has this type, whereas in TypeScript you can say JSON.parse and now the
[00:17:56]
type is any and you can just say, oh yeah, it's a string. Oh, it's this kind of object. Oh, it's a
[00:18:00]
list of numbers and it'll trust you. So yes, the TypeScript type system strictly has more features,
[00:18:07]
even though it's not sound in the sense that Elm is. So this discriminated union feature
[00:18:16]
is what you work with when you're sending a value from Elm to TypeScript with elmts interrupt.
[00:18:24]
So you have a single port. So you have a port called interrupt from Elm. So you would say
[00:18:31]
app.ports.interrupt from Elm. And what elmts interrupt gives you is it generates a TypeScript
[00:18:39]
declaration file that's going to tell TypeScript what types you're going to receive when you're
[00:18:46]
wiring up your Elm application and what types you need to send and that it can accept when
[00:18:51]
you're sending ports from TypeScript. So now when you say app.ports.interrupt from Elm,
[00:18:59]
that's the single port that you use for elmts interrupt to receive an incoming value from Elm.
[00:19:05]
And now you can say in that function, you receive your from Elm payload, your from Elm value,
[00:19:12]
and you can say switch from Elm.tag. And now you're going to have report analytics and it's
[00:19:19]
going to have the payload with whatever analytics information. And that can even be whatever custom
[00:19:24]
type and you know and so on and so on and so on. You can have arbitrarily complex type information
[00:19:31]
expressed there. So the really cool thing is that anything you can describe all sorts of information
[00:19:39]
about these JSON values through the TypeScript type system. So that's the experience you end
[00:19:45]
up getting with elmts interrupt. So let's see, we should probably talk a little bit about how that
[00:19:52]
actually happens and how you end up with TypeScript type information with this new elmts interrupt
[00:19:58]
approach. Because we said that unlike the old deprecated elm typescript interrupt, the new elmts
[00:20:05]
interrupt does not use these implicit serialization, deserialization functions by implicitly describing
[00:20:13]
a port with an elm type. And you mentioned we're using JSON decoders and JSON encoders.
[00:20:21]
That's right. Is that right? Yes. That is, well, yes and no. So elmts interrupt, there are a lot of
[00:20:32]
terms here, but I can tell you from using elmts interrupt, the experience of using it and my
[00:20:40]
sort of goal for the developer experience here is that you sort of tweak a decoder or you tweak an
[00:20:46]
encoder and that type information flows through to TypeScript and you have, you know, it's this sort
[00:20:52]
of types without borders idea I talked about in my elm conf talk, types without borders. That's
[00:20:57]
the goal. So elmts interrupt uses an elm package that I published under the hood and that's what
[00:21:04]
you're writing that gives you the information about the data you're sending and receiving
[00:21:10]
in your ports. So the package is called elmts.json under Dillon Kearns. That's right. Dillon Kearns
[00:21:17]
slash elmts.json. So you can find a link in the show notes. That is the elm package and what that
[00:21:24]
package is is largely it is a drop in replacement for elm.json, json decoders and encoders.
[00:21:34]
All right. So that's pretty boring as some people would like to say. That's right. Yes, it is.
[00:21:42]
Yes, it is extremely boring. In the good way. In the good way. In the good way. It is, it's sort of
[00:21:49]
like a concept. This was one of like my favorite things about coming up with this design was just
[00:21:54]
pulling on thread more and more and seeing that this very simple idea, things just sort of fall
[00:21:59]
into place. So because there's really the underlying idea is not very magical or complex.
[00:22:07]
So the underlying idea is that you have these decoders. So if you say, so instead of saying
[00:22:14]
import json.decode and then saying json.decode.string, right, to get a string decoder in elmts.interop
[00:22:22]
with that package, you would say import or sorry, with elmts.json, which is the elm package, you
[00:22:30]
would import ts.json.decode and then you would say ts.json.decode.string. That so far, it's
[00:22:40]
pretty much exactly the same except the import as a json.decode, right? Yeah. It also gives you a
[00:22:46]
decoder, but from ts.json.decode. That's right. That's right. It gives you, yes, exactly. It gives
[00:22:52]
you the same type of decoder string. So it's going to decode into a string. The difference is with
[00:22:58]
json.decode, you can just run the decoder with ts.json.decode. You can run the decoder and there's
[00:23:06]
actually just a regular elm.json.decode under the hood, but you can also ask that decoder for its
[00:23:12]
TypeScript type. Oh. And there's the magic. There's the magic. That's the only piece of magic. Now,
[00:23:19]
as a user, you don't actually ever do that, but the tooling, so there's this elmts.interop.npm
[00:23:26]
package. And what that does is it leverages that by asking your ts.json.decoders for their type
[00:23:37]
information. I might be going on a tangent here, but I'm curious, how does it do that? How does it
[00:23:41]
go and fetch all those? Right. No, it's a great question. It's not a tangent at all. So yeah,
[00:23:47]
let's talk about that. So what it does is you define a module, Interop definitions.
[00:23:55]
Does it need to have that name? So the way I'm doing it in the CLI,
[00:24:00]
it uses that name by convention, and then you can change the input file using a CLI flag.
[00:24:07]
So, but the main idea is that you define your flags, your elm to JSON ports, and your elm to
[00:24:19]
TypeScript ports and your TypeScript to elm ports. And you define a decoder for the flags and you
[00:24:26]
define a decoder for your elm to JSON ports, for your elm to TypeScript ports. The way it works is
[00:24:33]
you have your Interop definitions module. And in that you just expose a top level record called
[00:24:40]
Interop. And that has a to elm, from elm, and flags. Those three values are just going to be
[00:24:48]
ts.json encoders and ts.json decoders. So for the flags, you need a ts.json decoder because
[00:24:57]
you're decoding values from TypeScript to elm. For the to elm, which is going to be values flowing
[00:25:04]
from TypeScript to elm, you also define a decoder. And those are going to be wired in, of course,
[00:25:12]
with subscriptions. And then for your from elm, which is going to be your ports going from elm
[00:25:18]
to TypeScript, you define an encoder. So those are the three things that you give to your Interop
[00:25:25]
definitions. And what those give you is, so elm ts Interop, the CLI, takes that one module,
[00:25:32]
which contains all of the information about your Interop, your flags, your ports to elm,
[00:25:38]
your ports from elm. And that contains all of the information about the types that will flow in and
[00:25:45]
out through your Interop. And so all it does is it just takes that and it takes the encoders and
[00:25:51]
decoders that you define there, and it creates a little elm app that just imports that value.
[00:25:56]
And then it uses that to generate to ask what the TypeScript types are just using that public API
[00:26:04]
that's part of the ts.json package. So that's all the magic that there is. And then from there,
[00:26:10]
it builds your generated TypeScript declaration file, which gives you the IntelliSense and type
[00:26:16]
safety in your TypeScript code. So that's all it is. And then there's a module that you use to
[00:26:23]
sort of encapsulate that and use that Interop and you send your ports through there. So you don't
[00:26:30]
actually expose your ports. You don't define your own ports. You use these elm ts Interop ports,
[00:26:37]
and it sort of wraps things for you and passes things through those ports.
[00:26:41]
Gotcha. All right. So let's go back to ts.json maybe. Sorry. So you mentioned you have decoders
[00:26:49]
and encoders, which look very similar to json.decode and json.encode. So you gave the
[00:26:55]
example of decode.string. And I'm guessing that you're using the same construction mechanisms.
[00:27:04]
So you can compose them together just like we used to with json.decode and encode.
[00:27:09]
Exactly right. And you can even say, so if you say decode.field first name, decode.string
[00:27:16]
with ts.json, import ts.json.decode as decode. Let's just call it ts.decode as a convention.
[00:27:24]
All right.
[00:27:24]
Then that knows that the TypeScript type you're going to get from that decoder is an object with
[00:27:32]
a field first name that is a string. It knows that, right? I mean, it's not too big of a leap
[00:27:37]
to understand how you could say decode.field. And now the TypeScript type that that will
[00:27:44]
successfully decode is going to be an object with a field of whatever decoder you had. In this case,
[00:27:51]
ts.decode.string. So it's really like it feels magic, but it's actually just this simple idea
[00:27:58]
with it wired up in one place. And this is one of the things that's a big improvement over the old
[00:28:05]
Elm TypeScript interop approach, which is that that static analysis approach would slow down as
[00:28:10]
a code base grew. So some users were reporting performance problems with large code bases.
[00:28:18]
But with Elm TS interop, since there's really no magic here, you're just defining all of your
[00:28:26]
decoders and encoders for interop in a single module. The performance is constant time. It
[00:28:32]
doesn't scale as your code base scales. And it's, I'm guessing, very fast because it's a
[00:28:36]
very small program. Exactly. It's just there's really not that much to it. So it's extremely fast
[00:28:41]
and reliable. I'm guessing you could have tried to do the same thing with static analysis,
[00:28:49]
but the problem with that approach is that sometimes things would get very complex and it
[00:28:54]
would be very, very hard for you to gather the information as reliably as you can with this
[00:29:00]
approach. Oh, you're saying like static analysis of your JSON decoders? No, of the Elm code, sorry.
[00:29:06]
Of which Elm code in particular? So with the Elm TypeScript interop, you would look through the Elm
[00:29:13]
code, you would look for the ports. And then you could see, oh, it uses decoder value. Yes,
[00:29:22]
that's right. Okay, let's look at where that is used. And now let's try to find out what fields
[00:29:28]
are expected in those decoders. Right. So you could do it, but it would be based on how it's
[00:29:34]
like JSON decoder or something. Yeah, that's the thing. And the thing is like if it's complicated
[00:29:39]
for the program, for the tooling to be built that way, then it's complicated to understand because
[00:29:45]
there's this magic that's hard to trace. And if something goes wrong, you don't know where it went
[00:29:50]
wrong or what you're doing wrong. So I really liked the simplicity of this where you're just
[00:29:56]
defining a JSON decoder. Also, it works surprisingly well. So let's talk about a little
[00:30:06]
bit more of the TS JSON API, just to kind of get into where it gets interesting. Because if you're
[00:30:12]
saying tsjson.decode.string, that's cool, but that's not that exciting. It's like, okay, now you
[00:30:24]
know that there's a string going through, you know, and you build up objects by saying decode.field.
[00:30:29]
And now it knows that it's got a field of a particular name of a particular type. That's
[00:30:33]
getting more interesting. You can do that with lists and different data types. But where it gets
[00:30:39]
really cool is one of so now what happens if you say so like, what happens if you say decode.oneof
[00:30:46]
tsd code.oneof and then you have two values in that one of list, you have tsd code.field first
[00:30:57]
tsd code.string and you have tsd code.field first name tsd code.string. So you've just
[00:31:05]
described to you know, maybe you have like backwards compatibility with a different type
[00:31:10]
that you used to send, right? So now, do you know what the TypeScript type annotation would be for
[00:31:16]
that port now? So it would be an object with the first property, which is a string, or it would be
[00:31:22]
an object with first name, which is also a string. Exactly. So it's a union type. It's not a
[00:31:30]
discriminated union like an Elm custom type, but it's just a straight up union of like, it can be
[00:31:35]
these two different types. So that's how you build unions with, you know, the TS JSON decode API.
[00:31:42]
It's just you say one of which is so easy to write and gives you this incredible expressive power.
[00:31:49]
So I really like how that lined up. Now, the other ingredient that I think starts to make it really
[00:31:56]
interesting is the TS JSON decode literal function. Oh, yeah, I've seen that one. So that is to build
[00:32:05]
those type literals, right? So it's Dillon or Yeroon. Exactly, exactly. We talked about the literal type
[00:32:12]
Dillon or Yeroon, which is just two literal strings. So you could do that with, so you would do
[00:32:18]
TS JSON dot one of, and then you would do TS JSON dot literal, and then you give it a JSON encode
[00:32:26]
value. So you could just give it a JSON encode value, JSON dot encode dot string, Dillon, and then
[00:32:32]
you do another TS JSON literal, and you'd give that JSON dot encode dot string, Yeroon. Notice, it's
[00:32:39]
just a JSON encode value. So it could have been any encoded JSON value there, but often it's just
[00:32:45]
going to be strings or ints or something like that, but it can be anything. So now we've just
[00:32:49]
defined that literal type. And also notice that if you wanted to do a discriminated union,
[00:32:54]
that's the building block you have. So now if you wanted to do that discriminated union where you
[00:32:59]
said TS JSON dot, so where we had like a discriminated union user, type user in TypeScript,
[00:33:08]
type user equals object kind colon guest string. Guest is a string. That's a literal string. Or
[00:33:15]
object kind colon admin. And then we also have an admin ID is a number in there, right? So if you
[00:33:23]
wanted to do that, now you could say, you could use TS JSON dot one of, and you would say TS JSON dot
[00:33:35]
field kind, and then you'd use a TS JSON literal of guest. Now you've just defined that guest
[00:33:42]
variant of that discriminated union. And then you do a similar thing for defining the admin type
[00:33:49]
with the admin property. And now you've just told TypeScript that what type it should expect to send
[00:33:55]
or receive there. And you've told Elm what data type it's going to receive as well.
[00:34:01]
Yeah. So if you want to do any kind of union discriminated or not, you use one off, right?
[00:34:07]
Yes. For decoding, that's right. Exactly. If you want to end up with a TypeScript union type,
[00:34:14]
then you use TS JSON dot decode dot one of that's right. So something about using decode one off,
[00:34:23]
or what about discriminated? Oh, but the Elm now knows about the type. That is what it was. Okay.
[00:34:30]
Yeah. So this is like, this is one of the most important points I think about this approach.
[00:34:36]
So with the old Elm TypeScript interop approach, the source of truth source of truth is really
[00:34:42]
important in general with this sort of concept of like types without borders. I'm not sure how much
[00:34:48]
my hashtag types without borders is catching on, but I at least like having that idea in my brain
[00:34:53]
of like, you know, being able to have types that flow through these different boundaries of
[00:34:59]
different languages and runtimes and whatever without losing information about what, what types
[00:35:04]
are passing through. And so in, in this concept of types without borders, there's like a very
[00:35:10]
important point of what is the source of truth? Yeah. With Elm GraphQL, the source of truth is
[00:35:16]
the GraphQL schema. Now actually that's oversimplifying because well, what's the source
[00:35:22]
of truth of that GraphQL schema? The thing that generates that schema. The thing, exactly. So
[00:35:28]
if you're using GraphQL JS or something like that, then you've got like a schema definition language
[00:35:34]
that where you describe the types and then you just send data through your, your endpoints. And
[00:35:39]
hopefully the types match up. And if they don't, the GraphQL JS server will say that something
[00:35:45]
went wrong. So the source of truth is just sort of what you write in that, in that file that describes
[00:35:51]
the schema. If you use, you know, a TypeScript package like Nexus, that's what's called a code
[00:35:57]
first approach to GraphQL. So now you're writing code for, for sending values out and that code
[00:36:05]
is the source of truth for the schema. So sometimes they call this code first, you know,
[00:36:10]
schema first or code first approaches to GraphQL. If you're using a tool like Hasura or PostGraphile,
[00:36:16]
then the source of truth is your Postgres database. And that database, those tools will take
[00:36:23]
your database schema and turn that into the schema. So many layers of indirection. Right.
[00:36:29]
I mean, it works nicely, but yeah. Yeah. You can derive so many things from one thing,
[00:36:36]
which is very nice. Yes. It can also create problems, I guess. Well, that's right. And that's,
[00:36:41]
that's exactly the challenge, right? Is whatever that source of truth is, and this can happen with
[00:36:47]
these sort of database schema driven GraphQL tools is sometimes it can become too low level
[00:36:53]
because you're coupling it to the sort of schema. Now there are ways to avoid that by using sort of
[00:36:59]
higher level tables and having tables that are low level details that don't end up in the public
[00:37:05]
facing API. You can define, you know, you can define Postgres functions and use those as a,
[00:37:12]
a way to do higher level things. But all that to say, if you're lost by the specific GraphQL jargon,
[00:37:19]
that's, that's not the important thing. The important point is what is the source of truth?
[00:37:24]
And this is something I've been thinking about. So with the old Elm TypeScript interop approach,
[00:37:29]
the source of truth was the types that you just, that you wrote in your port. So when you say port,
[00:37:34]
port send user, and then you have a record, well, there's one problem, which is you can't send a
[00:37:42]
custom type through that port because, you know, Elm doesn't have a way to describe, to describe
[00:37:49]
custom types in a serializable way. So you're stuck with records, strings, ints, these basic types.
[00:37:55]
Yeah. So you're over generalizing. Yeah. You could be more precise if you had more knowledge,
[00:38:03]
but you lost it somewhere. Exactly. It's lossy. It doesn't have the full expressive power of the Elm
[00:38:09]
Type system. So that's, that's one problem, but, but that's actually not the problem I'm talking
[00:38:13]
about. The problem I'm talking about is that now what is the, what is the resulting TypeScript type?
[00:38:20]
Well, it's whatever you end up with by automatically serializing that Elm type. So forget
[00:38:25]
the limitations of that automatically serializable Elm type, not being able to represent any Elm type,
[00:38:30]
but now your source of truth is that, that Elm type. And so even if you could describe any Elm
[00:38:38]
type, even if you could, let's just say you could automatically serialize a custom type in Elm
[00:38:44]
using a port. So you could say, you know, you have type user is guest or admin, admin ID, whatever
[00:38:51]
fancy Elm custom type you have. Let's say you could send that through a port directly and Elm
[00:38:55]
auto serializes it. Well, what's the serialization format? Because is that how you want it in
[00:39:00]
TypeScript? So there's the issue. Now your source of truth is the Elm type and that's the first
[00:39:06]
class citizen. But what does that mean for the TypeScript type? It's a second class citizen.
[00:39:10]
That's, that's an inherent problem. So what I've realized is that one way to approach this problem
[00:39:17]
is to make the first class citizen, the serialization or deserialization layer,
[00:39:22]
the sort of adapter. Because if you think about a decoder, what does a decoder describe as the
[00:39:29]
source of truth? Does it, is the source of truth, the Elm type you're going to end up with, or is
[00:39:34]
the source of truth, the JSON data that you're going to receive? The JSON data that you're going
[00:39:38]
to receive. I would argue that the source of truth is the translation layer, which represents both.
[00:39:46]
So that's the really cool thing. Neither one is a second class citizen. You tricked me. I tricked
[00:39:52]
you. That's your question. So, so the, uh, you have not lost the ability to express whatever type
[00:40:01]
information you want from the JSON TypeScript side and whatever information you want to turn that
[00:40:07]
into on the Elm side, you're completely able to express both of those. And you can, because of
[00:40:12]
course you can use your decode dot map helpers and all those fancy things to turn into whatever
[00:40:18]
Elm types. And, and you can use this, uh, TS JSON decode API to expect arbitrary JSON data with
[00:40:27]
very intricate constraints described, like the literal types. You can have object properties,
[00:40:33]
union types, even intersection types. You can, like, you can really constrain your JSON in pretty,
[00:40:41]
pretty powerful ways using, using that type system. So what you end up with is types without borders,
[00:40:47]
where the source of truth is the thing that describes that serialization or deserialization,
[00:40:53]
which means, so if you think about that flow, what that means is, uh, let's say that you
[00:41:00]
change your decoder, right? You change your decoder and then you rerun the Elm TS interrupt CLI tool.
[00:41:06]
Then what's going to happen is you, you're going to end up, you know, you regenerate your TypeScript
[00:41:13]
declaration file with the CLI tool. Now you, you're, you've ended up with different TypeScript
[00:41:18]
types. So now you're going to get a TypeScript error or, you know, or some error in Elm where
[00:41:24]
the types have changed. And so you have, you have to go and fix those, but the source of truth is
[00:41:30]
like the actual thing that's, that's handling that translation. So that's always going to be correct.
[00:41:36]
And it's always telling both sides what, what should happen. It's like, Hey, Elm code, uh,
[00:41:41]
so this decoder just changed and, uh, you're now going to receive this type, right? That's what Elm,
[00:41:47]
Elm's type system gives you, which we love. Well, you get that same effect on the TypeScript side,
[00:41:52]
where you change the expecting TypeScript types and TypeScript is like, Hey, uh, so did you notice
[00:41:59]
that you, you're wiring in the wrong type because the decoder just changed? It's a, it's a lot to
[00:42:04]
wrap your brain around, isn't it? We haven't even talked about encoders. So I'm guessing it's pretty
[00:42:11]
similar in a way. Well, uh, so encoders are similar, but there's one distinction that I,
[00:42:19]
that I want to point out. Cause I think a podcast is a good medium for this. And that is,
[00:42:23]
so notice that it's the type that you have is encoder, not json.encode.value. So,
[00:42:31]
yeah. So in the, in the Elm slash json, we have decoders, but we don't have encoders.
[00:42:37]
Exactly. At most we have a function that takes some data in terms of two,
[00:42:43]
json.encode.value. Exactly right. And so this is like a really, um, I mean, I hope that this
[00:42:50]
concept is helpful for somebody getting started with Elm TS Interop. Um, I think that just having
[00:42:55]
this mental model can help. So just conceptually, if you wanted to get type information about a
[00:43:02]
json.encode.value that you, you do, you know, json.encode.object and then you create a bunch
[00:43:08]
of fields. Well, what, what keys does it have? I mean, what if you do an, a conditional in there
[00:43:14]
and you, you do one key in one case and another key in another case, what if you encode to a string
[00:43:21]
in some cases and an objects in other cases, there's nothing stopping you from doing that.
[00:43:26]
So what's, what, what's the type that is going to result in? You don't know. It's just an encode
[00:43:30]
value. It could represent any type. And you don't, you don't know until you pass a val,
[00:43:35]
an Elm value to encode it. Yeah. And it's totally opaque. Like the only way to extract information
[00:43:40]
is to try and decode it. Exactly. And to do that, you need a value, but what, what value are you
[00:43:46]
going to send through it? It could be an infinite number of values, you know, so that that's not
[00:43:51]
going to work. So that's the, uh, that's the trick with this API is that it's not an encode. It's not
[00:43:58]
a tsjson.encode.value. It's a tsjson.encode.encoder. And that represents a function and it knows what
[00:44:08]
Elm type it's going to receive. And it only handles one particular Elm type. So in the case of,
[00:44:14]
you know, our, uh, our TypeScript discriminated union type user, which is a guest or a regular
[00:44:20]
user or an admin user, we've got, you know, let's say that we're serializing that from
[00:44:25]
a similar Elm type, which is an Elm custom type, the equivalent of that TypeScript discriminated
[00:44:30]
union. So you would write an encoder. I mean, you can imagine writing a plain vanilla Elm,
[00:44:37]
json encode, you know, encode value, a function that takes that Elm user type and encodes it into
[00:44:46]
an encode value. And it's going to say, you know, case user of admin, grab some admin information,
[00:44:53]
encode.object, yada, yada, yada. Well, it's similar with, with a tsjson encoder, except that
[00:45:01]
it's not just by convention that you're making it a function. The encoder itself represents a
[00:45:06]
function. So, uh, so, you know, you can say union, uh, you know, ts, ts encode, let's call it it's
[00:45:14]
import tsjson.encode as ts encode. So now you could say ts encode.union. And the first thing you give
[00:45:22]
it is a function. And in that function, you're going to do a case statement on your user type.
[00:45:28]
Where's the user type coming from? Well, the encoder you're defining represents taking a value
[00:45:34]
and serializing it. So it's not a, it's not an argument that comes from the function you're
[00:45:38]
defining. The thing is that you give it that, you know, probably an anonymous function in that
[00:45:43]
context and describe how to take that thing and turn it into an encode value. Yeah. It looks a
[00:45:50]
lot like the codecs that we talked about during the Elm codec episode. That's right. It does look
[00:45:56]
very similar to the codec. So yeah. So if you're used to that pattern, it's the same. Right. So
[00:46:02]
one of the tricks here, it's worth looking at the docs for this because it might take a little while
[00:46:07]
to wrap your brain around, but I think it's always, you know, this is one thing that we kind of come
[00:46:10]
back to pretty often on this podcast is to understand API design for a particular package
[00:46:17]
in Elm. It's often useful to sort of understand the problem that the API designer was trying to
[00:46:22]
solve with that. Well, I have firsthand experience on this API, so I can give a little insight into
[00:46:28]
the problem that this is solving. So the first one I described, which is how do you get the type of
[00:46:33]
an encode value? You can't, you need an encoder. The second thing is how do you know? So like now
[00:46:41]
you're describing a union encoder. And so union is the word I'm using because it's going to encode
[00:46:50]
it into a TypeScript union type. And so how do you describe that? How do you know the possible
[00:46:57]
things you might encode into? Because now if you say, okay, I have a function case user of,
[00:47:03]
and then you just give it a JSON encode value. Well, now you still have the same problem that
[00:47:08]
you could encode it into any value. However, with the design of this API, and this was inspired by
[00:47:15]
Elm Kotick and Mini Bill Leonardo's work on that, which so big thanks to him. I had some
[00:47:22]
conversations with him and he really helped me kind of have some of these insights for this
[00:47:26]
particular part of the API. And so now you have to, I think of this as like registering the different
[00:47:35]
possible encoders in that union. So now you have to define these different encoders. And so you say,
[00:47:41]
you know, variant, variant literal or variant object, or you can have these different variant
[00:47:46]
types. And that's sort of like registering something that you're going to receive in that
[00:47:51]
anonymous function. And you can use that when you when you say case user of you can use that to,
[00:47:57]
to encode the user. But why can't you just do any JSON encode value? Well, the reason is because
[00:48:03]
you need to, you need to register upfront all the possible things you might encode into so that
[00:48:10]
Elm TS Interop can know those types and create the appropriate union type. Because otherwise,
[00:48:16]
it could be any JSON value, you don't know what it's going to be. So that's a lot to unpack.
[00:48:22]
Yeah. But but overall, the the package looks very similar to JSON to Elm slash JSON,
[00:48:28]
with the additional of some functions to better reflect or better decode the TypeScript possibilities.
[00:48:36]
That's right. Yeah. So also looks a bit like codec in some instances. And the obvious additional
[00:48:44]
feature is that you can get the TypeScript type that you can extract that out of the encoders
[00:48:50]
and decoders. Yes. Yeah, you'll probably get pretty far with this library before you
[00:48:56]
realize where it diverges from the Elm JSON API. Yeah, I mean, chances are, I guess, I guess you
[00:49:02]
might be sending some sophisticated custom types through ports. I mean, that's like one that's one
[00:49:06]
of the main goals of this project was people kept saying with Elm TypeScript Interop, how do I
[00:49:11]
serialize custom types? And I was like, hmm, I've got some very fancy ideas for how I might do that.
[00:49:18]
And they all just felt like, like, okay, I could do that. But yeah, it's like so complicated. And
[00:49:24]
how the developer experience was confusing, because there are all these specific things you have to
[00:49:29]
learn and these conventions you have to rely on. And then not only that, but you have this problem
[00:49:36]
of, you know, which type is the first class citizen. So you have Elm types, and you automatically
[00:49:40]
serialize these things. And what if you have opaque types and all these challenges? So this,
[00:49:47]
I'm a lot happier with this design. So I'm curious, can you use Elm TS JSON in any way
[00:49:53]
without Elm TS Interop? Does it have any value? Yes, that's one of the reasons why I named it
[00:50:00]
Elm TS JSON, not Elm TS Interop. I originally was calling the package itself TS Interop. But first
[00:50:06]
of all, I think that the name reflects what the package does a little bit better, because it's
[00:50:11]
just it's just JSON encoders and decoders, except that they're TS JSON values. So you can ask for
[00:50:18]
the type information. But also, I discovered that I can use it to do a lot more than just Elm TS
[00:50:25]
Interop. So one of the things that I've been exploring also, I'm not quite ready to release
[00:50:31]
this, but hopefully I'll make an announcement soon. I've been playing around with something
[00:50:36]
that allows you to use this same underlying package and similar techniques to have a type
[00:50:42]
safe bridge, this sort of types without borders concept with interacting with serverless functions.
[00:50:50]
So the basic use case would be you have a serverless function, which is just a, it's just a
[00:50:58]
JavaScript function. I mean, you can write them in different languages. And there are, you know,
[00:51:02]
there are some tools out there for using serverless with Elm. And that's, I think, an
[00:51:09]
interesting and useful approach too. But the idea is if you just want to write a serverless function,
[00:51:15]
serverless functions are a really nice way to just like call an NPM package. Like, for example,
[00:51:20]
if you're using Stripe to initiate a payment, or you're using Auth0 to authenticate someone or
[00:51:27]
whatever it may be, you just pull in, you know, you just want to use this Stripe NPM package. And
[00:51:35]
you also want to have like a server secret, which you cannot include in your client code because you
[00:51:41]
don't want them to be able to do payment processing. Yeah. So I'm sure there are some features or some
[00:51:50]
use cases for that. Yeah. Right. I think there might be like fraud. Right. Basic fraud. Yeah.
[00:51:59]
Yeah. I mean, it's really, it's because, I mean, this is the kind of thing, like, if you think
[00:52:03]
about it, it's like ports are really useful for certain cases. But then some cases you just,
[00:52:09]
you need to do it in a secure environment, or you need to do it in an environment where you,
[00:52:13]
you know, the code that's executing submission, that you're not bypassing some client side
[00:52:20]
validations, and you need to have the server as the gatekeeper or whatever, right. So, you know,
[00:52:25]
you want that to be the source of truth. So I actually built this project and have been playing
[00:52:32]
around with it where it uses Elm TS JSON to let you define a layer to perform HTTP requests to a
[00:52:42]
serverless function. In this case, I've been doing it with Netlify because Netlify serverless
[00:52:45]
functions are really easy. You have a folder that has a bunch of dot JS files that export a
[00:52:52]
function called handler, which returns status code 200 or whatever status code comma body,
[00:52:59]
and then a string. And you know, often you want to have that string be JSON string fight or whatever.
[00:53:07]
So I built this layer that keep uses that type safe bridge so that you get Elm functions for
[00:53:14]
making a request and response. And it uses Elm TS JSON to describe an encoder for the request it
[00:53:20]
will receive. So you're in your serverless function code, you know, the TypeScript type you'll end up
[00:53:26]
with that you'll receive from Elm. And then it knows what type you need to send back in TypeScript
[00:53:32]
based on the TS JSON decoder that you write. And if you don't send the right type, TypeScript will
[00:53:38]
yell at you. So I so I another thing I built with this, so the Elm TS JSON package has a function
[00:53:46]
that allows you to get the the TypeScript type. It also has a function that allows you to get the
[00:53:52]
JSON schema type. And so JSON schema is just using JSON values to describe JSON values. Is that weird?
[00:53:59]
It's JSON that describes the shape and is analogous to like using TypeScript to describe the shape of
[00:54:08]
JSON except the format is not a TypeScript string. It's a JSON value in this format.
[00:54:13]
But does it look like TypeScript or is it simpler?
[00:54:17]
I mean, it's uglier because it's JSON in a particular structure. It's like not the best to
[00:54:23]
handwrite. But that's the cool thing, right? Is you can use tooling to do this. But the really cool thing
[00:54:28]
is that I use in this sort of alpha tool that I built that I haven't publicized yet.
[00:54:36]
What happens is it uses the JSON schema values of the encoders and decoders to scrub and validate
[00:54:45]
the server inputs because you don't want to accept server inputs. It could be a security problem
[00:54:50]
to send data that you didn't expect to send and to receive data that's not the types you expected
[00:54:56]
to receive. So it's not enough to just have TypeScript types that sort of tell you what the
[00:55:01]
types are because you want to actually ensure that those types are correct.
[00:55:06]
So you're sending an encoder value?
[00:55:10]
Yeah.
[00:55:11]
But the additional fields that are not necessary are removed from the payload.
[00:55:16]
Exactly. Those will get stripped out. And if you say you're sending a string and you send a list
[00:55:21]
or vice versa, it will give an error instead of calling that serverless function.
[00:55:29]
And the really cool thing is the code to do that is very minimal. It's just sort of a nice
[00:55:36]
wiring for this that leverages that technique. So that's another tool I've been working on.
[00:55:41]
But just the general, I built the TSJSON package for tooling. So that's sort of the point is that
[00:55:49]
it gives tooling a very simple way to sort of introspect the types.
[00:55:53]
Gotcha. I'm wondering, would you use Elm TSJSON instead of Elm JSON for anything?
[00:55:59]
Because it can do the same thing, but it can also do more.
[00:56:03]
Yeah. I would probably pull it in as needed. So I was starting to use it with Elm Pages under
[00:56:11]
the hood because Elm Pages has a fair number of ports that I use to get meta tags and information
[00:56:19]
from the application and then generate the HTML for that. And so I was starting to convert my
[00:56:26]
decoders and encoders to use Elm TS Interop and Elm TSJSON. And it works nicely because if you
[00:56:33]
need a regular JSON encoder or a decoder, you can just turn your TSJSON one into that. Actually,
[00:56:41]
one thing I started doing there was I started to build, it's not quite ready to be published,
[00:56:46]
but I started to build a codec version of TSJSON because it turns out originally I didn't build
[00:56:52]
a codec version of the API because I figured most of the time you're like, hey, TypeScript,
[00:56:57]
here's this data or hey, Elm needs this data from TypeScript and you don't necessarily need to go
[00:57:02]
both ways. But I ended up building it because I realized with Elm Pages, I use Elm program test
[00:57:08]
heavily in Elm Pages. And so with Elm Program Test, you can test your ports. But to test your
[00:57:17]
ports, you need to give a decoder that tells you how to deserialize the JSON that you send through
[00:57:23]
your ports. So I need to go both ways. So I've been using codecs in order to do that and I realized
[00:57:30]
I needed that. I think that will be a nice addition. Yeah, I think so too. It's nearly done.
[00:57:37]
I got most of it pretty easily. It will be in Elm TSJSON also. It would not be a separate library.
[00:57:43]
Exactly. It'll just be part of it. That's one of the really cool things. It's just whatever
[00:57:46]
that library does, the key thing is given an encoder or a decoder, can you get the TS type?
[00:57:52]
And then anything else the API adds or changes, that's the core detail that the tooling around
[00:57:58]
it like Elm TS Interop needs to leverage it. I imagine that we can use the same technique for
[00:58:05]
any kind of format. So you're using it for JSON, but someone could do something similar for
[00:58:11]
Protobuf or XML. I don't know if there's an XML schema. Soap, simple object something something.
[00:58:24]
It's very simple. You'll love it. Yeah.
[00:58:27]
I prefer staying dirty. Let's put it that way. Yeah. But yeah, you can use the same technique.
[00:58:36]
You would just write a different encoder, a different decoder, but you could use the same
[00:58:42]
thing, right? I think so. I think I'm certainly going to be looking for other places to apply this
[00:58:48]
general pattern of... Because this is one of the cool things about Elm APIs is you have all these
[00:58:54]
guarantees as you build things up in Elm APIs. You know somebody's not going to sneak in some
[00:59:00]
value that isn't possible with your API. So you can prove that you're going to have correct
[00:59:06]
TypeScript type information or whatever metadata you want to describe about these values. So I
[00:59:12]
think it's a very cool general concept that I totally agree can be applied to many other areas.
[00:59:18]
So are there any pitfalls that you have seen that you have not been able to avoid using
[00:59:24]
API design? How can people misuse the tools in a way that they did not expect? Do you need to
[00:59:32]
test your decoders or encoders in a specific way? Well, the really cool thing is when you're trying
[00:59:39]
to keep a server and client in sync, there's a whole class of problems there between versioning
[00:59:46]
strategies and all of that. With ports, with interop, you don't have to deal with that class
[00:59:51]
of problems so much because it's just the client. Yeah, you usually ship both the Elm code and the
[00:59:57]
JavaScript code at the same time. Exactly. So you know that they're in sync. There's not this
[01:00:03]
possibility of having to bridge between two different versions and have that compatibility
[01:00:08]
layer. So that whole area of complexity is completely avoided in this area. So that's the
[01:00:16]
cool thing, right? You write your TS JSON encoder and decoder and now you've got them in sync and
[01:00:25]
you address any errors and then the next version of the client you ship is going to be working with
[01:00:30]
those assumptions. Of course, you can still read something from local storage and assume the types
[01:00:35]
of it and that sort of thing. But that's just a general challenge with TypeScript. So I would say
[01:00:41]
probably the biggest pitfall is just that you're working with TypeScript. So TypeScript, I think we
[01:00:47]
could easily fill a whole episode talking about TypeScript. But in a nutshell, I think that
[01:00:54]
TypeScript is extremely well designed for the goals of the project. That said, the goals of the project
[01:01:02]
limit its ability to have a town sound type system and give guarantees in the way that Elm does.
[01:01:09]
Yeah, that is not one goal of TypeScript. Right, right. It's more attempting to fit into existing
[01:01:17]
JavaScript systems and conventions and all of that. And it does a very good job within those
[01:01:24]
constraints, but it means that it can't guarantee as much. And so I wrote a post which I referenced
[01:01:30]
earlier in this episode called TypeScript's blind spots. And that's linked to in the show notes. I
[01:01:36]
think that's worth a read just to understand when you can rely on TypeScript and when you can't,
[01:01:41]
just so you know what to be extra careful with. Also, I think it's important to just make sure
[01:01:46]
that your CI process is set up to make sure you're checking your TypeScript carefully and running it
[01:01:52]
in your build. And also, I kind of talk about this in the TypeScript's blind spots blog post
[01:01:58]
that there are some things that make TypeScript a little bit safer. So for example, you can use
[01:02:04]
a TypeScript ESLint check that will ensure that you're doing exhaustive switch statements.
[01:02:09]
Static analysis. Yeah, yeah. So that's a really good static analysis rule that makes it a lot
[01:02:17]
more robust, especially with Elm TS interop. I'd say that's probably the biggest gotcha. Yeah.
[01:02:24]
I had a few questions or notes. So you mentioned that there's only one output port and one input
[01:02:31]
ports. Can you add several ones? You can have a, maybe you could call it a legacy port. So
[01:02:39]
what Elm TS interop does is it gives you the generated TypeScript declaration file, which is
[01:02:45]
just a format that it's like a header file in C or something where you can describe the type
[01:02:52]
information of a JavaScript file. So the generated Elm JavaScript, it's just describing that. And
[01:02:58]
what that generated TypeScript declaration file from Elm TS interop does is it describes what it
[01:03:05]
knows about the ports that it controls, which is flags, the from Elm, you know, interrupt from Elm.
[01:03:12]
There's a port called interrupt from Elm and there's a port called interrupt to Elm. And it
[01:03:17]
describes those. And, you know, I think those names are unique enough that that won't be an
[01:03:21]
issue for most people. And then it says that the type of the ports is unknown for any other ports.
[01:03:30]
So it's not going to say, oh, those ports definitely don't exist. It's going to say
[01:03:35]
other ports may exist, but I don't know anything about their types. So it uses these unknown types
[01:03:41]
for those. Could you use Elm TS interop several times, one for interrupt definitions, that module
[01:03:47]
that we talked about and a second time with a different interrupt definitions module with
[01:03:54]
different name. What I'm too. Oh, did you have a use case in mind? Cause I've got, I've got.
[01:03:58]
Yeah. My use case would be just gradual migration. So I have plenty of ports and I want to use this
[01:04:08]
approach with Elm TS interop. Yeah. But I need to, I would like to get some of the benefits.
[01:04:14]
Yes. Yeah. I think that is a very good, very good approach. And I definitely, I would do the same
[01:04:21]
myself. So it works pretty nicely with that because those legacy ports, you can migrate off
[01:04:26]
them little by little. So, and then pull things into your interop definitions module. So that's
[01:04:31]
basically what I'll probably write up a little guide on how to do that and included in the show
[01:04:38]
notes. But I think that that's definitely like something that, that is going to work out nicely.
[01:04:44]
My migrating over incrementally. So ports are unknown and you can register, you can keep
[01:04:49]
registering the old ports and move them little by little and that's, that's going to work fine. So
[01:04:53]
like the default with Elm TS interop, you're probably going to be serializing and de serializing
[01:05:00]
union custom types at the top level. And so you just keep adding new custom types for the new
[01:05:05]
values you're going to send. Yeah. Also, we haven't talked about the pricing. And so I wanted to talk
[01:05:12]
about this early on, but there were just so many details to talk about that we kind of didn't,
[01:05:17]
didn't get there yet. Well, we actually didn't even mention that it was a paid product.
[01:05:21]
We did. Right. So it is the core stuff is all free and will always be that is the core stuff.
[01:05:28]
The core stuff is the Elm TS interop Elm package is.
[01:05:33]
Elm TS JSON.
[01:05:34]
Yes. Thank you. The Elm TS JSON Elm package is free and no strings attached. And you can use
[01:05:40]
that for other projects. The so I'm going to release a free version of the CLI at the time
[01:05:48]
we're recording, we're just wrapping up a closed beta. So that free version of the CLI is actually
[01:05:53]
going to do the things that we described on this podcast. So we actually haven't yet talked about
[01:05:57]
any pro features. So fortunately everything that we've talked about so far has been available for
[01:06:02]
free. Yay. Yay. Okay. So, so hopefully nobody's let down because you can do all the things we've
[01:06:10]
talked about so far. The pro version, and this was something very important to me. I wanted to
[01:06:15]
have the pro stuff be like extras for basically, if you get a lot of value from Elm TS interop,
[01:06:21]
if you rely on it for your business and you're like, Oh, this is this is really useful, then you
[01:06:26]
can, you can buy it if you'd like. And I hope, I hope the extra paid features are useful. So the
[01:06:31]
pro features currently include number one, a scaffolding tool, which is a web app that you,
[01:06:39]
you load in your browser, you give it a TypeScript types as input and it generates encoders and
[01:06:45]
decoders, including for discriminated unions. Like the one we talked about, the type user
[01:06:51]
holds discriminated union. It can do a pretty darn good job. Even with some complicated discriminated
[01:06:57]
unions like that, it even generates like Elm types, Elm custom types for you. And that's like
[01:07:02]
starting point. It's the cool thing is you take that scaffolded code as your starting point.
[01:07:08]
You copy it over. So now it's, now it's yours. Now you own it, not the scaffolding tool. And
[01:07:14]
you can change it all you want. And when you change it, the Elm TS interop, you know, generated type
[01:07:20]
definitions are going to reflect the changes you make to your code. So you own it, not the
[01:07:25]
scaffolding tool. Yeah. I've seen it in action and it generates a lot of code. There is value
[01:07:34]
in this tool. Yeah. I can tell. Yeah. It generates and it generates an encoder and a decoder for you.
[01:07:40]
So you can just type a TypeScript type and it gives you, and you can just copy whatever you
[01:07:46]
want from it and use in your code. So yeah, I think it'll be quite handy. Oh, you will need
[01:07:50]
to generate a codec now. I might. Yeah. I might do that too. Yep. Good luck. And maybe I'll add a
[01:07:57]
radio button so you can like pick which ones to generate to reduce the noise. But yeah, it's a,
[01:08:01]
I'm really happy with it. I think it's, I think it's a cool pro feature that you can do without,
[01:08:06]
you can do everything you need to without it, but I hope it is more ergonomic for people who find
[01:08:11]
value in the tool. And the other paid feature is an NPM package, which I give you access to when
[01:08:20]
you, when you pay. And what that does is rather than having a single custom type that it represents
[01:08:28]
your, you know, to Elm values, your from Elm values, you can define top level values in your
[01:08:36]
interop definitions module for encoders and decoders. So just like an Elm test, you can define
[01:08:42]
top level values of type test. The pro NPM package allows you to just expose top level
[01:08:50]
values of type encoder of type TS, JSON dot encode dot encoder and of type TS, JSON dot decode
[01:08:58]
dot decoder. And it'll automatically generate the code that's needed to encode each of those types.
[01:09:05]
So you don't have to do a big case statement yourself and write the top level encoder or
[01:09:10]
decoder. So it's just a little more ergonomic. Yeah. So it writes that complex codec for you.
[01:09:16]
Yeah. Yeah. And it's just a little more ergonomic to add a new one. You just, you know, you don't,
[01:09:21]
and you, you can you can just call. So it's going to give you a function for each for each top level
[01:09:28]
value you define in your interop definitions. It's going to give you a top level function
[01:09:32]
that takes that, that type and, and sends it directly. So you don't need to like build up
[01:09:38]
a custom type and send that through. So it's just a more ergonomic way of using it, but it's,
[01:09:44]
you know, nothing that you can't do without the free version, but I hope people find it useful.
[01:09:47]
Yeah. Do you think you will add additional paid features?
[01:09:52]
I'll probably at some point add some like video material about the, about some of the paid stuff
[01:10:00]
that paid users will get access to. And we'll see, we'll see if there's more, but I think,
[01:10:04]
I think that'll be a lot of, a lot of goodies for, for pro users.
[01:10:09]
Okay. So what is the pricing model?
[01:10:11]
I'm doing it as a single one time fee. You pay for it. It's yours. You're authenticated with
[01:10:18]
your GitHub account. It gives you access to the website. So you just log in with your GitHub
[01:10:24]
account and you get access to the scaffolding tool at elm.ts.interop.com.
[01:10:31]
Link in the show notes.
[01:10:33]
Link in the show notes. And you and then it automatically gives you, you know,
[01:10:37]
sends you instructions for setting up the pro NPM package and gives you access to that. So,
[01:10:43]
yeah, that's the pro package.
[01:10:44]
Very cool. Very cool. So if I, if I want to use this tool at work and I want my,
[01:10:52]
my whole team to use it, will every developer need to buy it?
[01:10:56]
Right. Good question. So the, I went with the simplest model that I could go with to start
[01:11:04]
just just because there, there are so many ways you could go with pricing and all that. And it's,
[01:11:10]
you know, it's a lot for, for me, it's a lot for, for users to think about. So I just wanted to
[01:11:15]
make it really simple. And so it's just one price per user. If you, and if you want to buy more
[01:11:23]
seats, if you want more users to have access to the scaffolding tool, then you buy more licenses.
[01:11:29]
Simple as that.
[01:11:30]
All right. And maybe don't buy one for the intern, I don't know, something like that.
[01:11:37]
Yeah. Hopefully if it's, you know, giving a lot of value, it's you know, I mean, it's,
[01:11:43]
it's not, it's, I think it's priced pretty, pretty fairly for you know, if you're relying on it
[01:11:49]
heavily for your project, I think the pricing is going to be quite fair.
[01:11:52]
And the core features are free. So.
[01:11:55]
That's right. Yeah. Yeah, exactly.
[01:11:58]
So I think the parts that you can rely on as a business are free and what is paid just adds
[01:12:06]
more value, but it is not necessary to work with it or to depend on it. Right?
[01:12:11]
Yes. Yeah, definitely.
[01:12:13]
That sounds very, no strings attached to me.
[01:12:16]
Yeah. That's, that was my goal. And I yeah, it's, so it's very exciting to release actually my first
[01:12:24]
paid product ever. You know, I've done services and, you know, workshops and that sort of thing,
[01:12:29]
but this is the first time I've ever created a paid product. So I'm, I'm really excited to,
[01:12:35]
to try that and see how the community responds.
[01:12:38]
Yeah, me too. Yeah.
[01:12:39]
I'd love to hear what people think. And I mean, I'm excited to see how this goes because I'm
[01:12:45]
hoping that I can just dedicate more time to building projects and, you know, monetizing them
[01:12:52]
directly so that I can build more of them. So.
[01:12:54]
Yeah, because the better it works financially, the more time you can spend building this tool
[01:13:02]
and other tools like it.
[01:13:04]
Exactly. Yeah. I would love to see more of that kind of thing in the Elm community so we can
[01:13:08]
invest more as a community and pushing things forward.
[01:13:11]
I'm very much looking forward to how this turns out.
[01:13:14]
Yeah, me too.
[01:13:16]
All right. So how can people get started with the Elm TS Interop?
[01:13:19]
First go to elm.ts.interop.com. I'll give a nice discount code that you can access,
[01:13:25]
you know, just put in your email and I'll send you a discount code there.
[01:13:29]
I'll have some resources for getting started with both the free and pro tiers.
[01:13:35]
And then other than that, also I, I wrote some blog posts back in December for the functional
[01:13:42]
Christmas that the consulting company Beck does. They do a lot of cool Elm stuff. So check those
[01:13:47]
articles out if you're curious to hear more about my thinking on this general approach to the tool.
[01:13:54]
I wrote one post that we'll link to about combinators and why that word sounds really
[01:14:01]
complicated, but I think is an important idea. And then I wrote a follow up post called Types
[01:14:07]
Without Borders Isn't Enough. And I really enjoyed sort of writing that and thinking about that.
[01:14:13]
I think that I kind of talk about some of the reasons why I think that Elm GraphQL has stood
[01:14:18]
the test of time as a solid approach and Elm TypeScript Interop needed revisiting. And I talk
[01:14:24]
about the reasons why. So, so give it a try and I'd love to hear what people think. And
[01:14:30]
well, until next time, talk to you later.
[01:14:32]
Until next time.
[01:14:33]
Bye bye.
[01:14:33]
Bye.