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

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

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


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