While waiting for Phoenix 1.7 and LiveView 0.18 to drop, I decided to play around with Remix a bit. My experiment was to build a magic link login in Remix, using Prisma as the ORM. I wound up getting burned pretty badly by tricky things in both these libraries that TypeScript was not able to catch for me. I want to share with you some of the lessons I learned while working on this.
loader
is a special function in Remix that runs on the client side before your page is rendered. It is similar to getServerSideProps
if you are familiar with NextJS, or a controller function in Phoenix or Rails. This code reads the sessionToken from the cookie, and then looks up the user from that sessionToken using Prisma ORM.
export const loader: LoaderFunction = ({ request }) => {
const session = await getSession(request.headers.get("Cookie"));
const sessionToken = session.get("sessionToken");
const user = await getUserForSession(sessionToken);
return { user };
};
export function getUserForSession(sessionToken: string) {
const session = await db.session.findFirst({
where: {
sessionToken: {
equals: sessionToken,
},
},
include: {
user,
},
});
if (session) {
return session.user;
} else {
return null;
}
}
After I got it working (meaning that I could log in and out from my computer), I decided to make sure the mobile page looked good. I used ngrok to tunnel localhost from my computer and opened the page on my phone.
I was logged in on my phone.
How could this happen? This was an app that I had just written. It was impossible for me to be logged in on my phone.
I’ll give you a moment to see if you can spot the problem.
**spoilers ahead**
There are a few culprits
First, session.get("sessionToken")
does not return a string, it returns an any
.
Even if I had typed it const sessionToken: string = session.get("sessionToken")
, TypeScript would have raised no errors, because string
satisfies the type any
. Instead, session.get
should return unknown
, instead of any
; since then I would have been forced to cast the type into a string. But this was library code. I could not change the type.
Secondly, the session.get
function returns undefined
instead of null
when the key cannot be found. The patterns for this vary across the TypeScript/JavaScript ecosystem, so I don’t really fault the library code. Nullish types are bad enough that some modern languages do away with them entirely (e.g. Haskell and Rust). JavaScript (and TypeScript) have 2 nullish types.
So, session.get("sessionToken")
returns undefined
.
No big deal, right?
Well, Prisma treats undefined
differently than null
. Here’s the documentation.
Prisma Client differentiates betweennull
andundefined
:
null
is a value undefined
means do nothing
So, my where clause (where: { sessionToken: { equals: sessionToken, }, },)
) was actually just doing nothing, exactly as Prisma documentation states.
So, what are the lessons we can all learn?
Even though it may seem obvious what a library is doing, and its API is very straightforward, you should always read its documentation. Reading about Prisma’s behavior around null
and undefined
, rather than just assuming, would have saved me a huge headache.
TypeScript, by a deliberate design choice, has an escape hatch called any
. If you (or any library you pull in) uses it, then you can no longer consider your code typesafe. Consider using unknown
instead. Inspect the types of any library code you are using, and if they return any
, consider casting the result to unknown
before handling it.