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: {

  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 between null and undefined:

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.

Joseph Lozano

Hash An icon of a hash sign Code Name
Agent 0097
Location An icon of a map marker Location
White Plains, NY