Utilizing Global Types for Thoroughly Typed Remix Applications

Utilizing Global Types for Thoroughly Typed Remix Applications

Table of contents

If you're one of the cool kids who use Remix.run, you'll know that (as of the time of writing this) v2 is just around the corner, and exciting stuff is on the horizon!

If you've been actively following Remix development, you probably already know that they decided to ease the transition from v1 to v2 by including future flags and allowing you to incrementally switch to v2.

One of the changes that occurred between v1 and v2 was a complete alteration to the meta function export typing, among other things. If you were using something along these lines:

import type { MetaFunction } from "@remix-run/node"; // or cloudflare/deno

export const meta: MetaFunction = () => {
  return {
    title: "Something cool",
    description:
      "This becomes the nice preview on search results.",
  };
};

You probably had to re-type your whole application to use the V2 type to switch to the v2 version of meta exports, like so:

import type { V2_MetaFunction } from "@remix-run/node"; // changed this

export const meta: V2_MetaFunction = () => {
  return [
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "Organization",
        name: "Remix",
        url: "https://remix.run",
      },
    },
  ];
};

If you were using this in many places you had to replace it everywhere. Or even if that wasn't the case, you might have been deploying it to Vercel for example and used the type like so:

import type { V2_MetaFunction } from "@remix-run/vercel";

You decided to change your runtime to be express, Cloudflare, or anything else... well now you have to do this everywhere:

- import type { V2_MetaFunction } from "@remix-run/vercel";
+ import type { V2_MetaFunction } from "@remix-run/express";

And even if this wasn't the case for your meta function, it sure is for typing your loaders/actions like so:

import type { LoaderArgs } from "@remix-run/vercel";

Well, in this article I will show you a neat little trick to make this easily switchable so if in the future there are some typing changes in v3, or you decide you want to move to a different runtime you can do so by just changing the typing in one place.

Declaration file

TypeScript has two main kinds of files. .ts files are implementation files that contain types and executable code. These are the files that produce .js outputs and are where you’d normally write your code.

.d.ts files are declaration files that contain only type information. These files don’t produce .js outputs; they are only used for type checking.

Let's use this fact to create our own declaration file and use it to type our Remix application.

First, create a index.d.ts file on the root of your Remix project. Then you can paste the following code into it:

// As long as Remix is using React this part shouldn't change
import type { useActionData, useLoaderData, ShouldRevalidateFunction as RemixShouldRevalidateFunction } from "@remix-run/react";
// These imports will depend on the runtime you are using, if it's node you
// don't need to change anything, if it's vercel for example, change to
// from "@remix-run/vercel";
import type {
  V2_MetaFunction,
  LinksFunction as RemixLinksFunction,
  LoaderArgs as RemixLoaderArgs,
  ActionArgs as RemixActionArgs,
  DataFunctionArgs as RemixDataFunctionArgs,
  HeadersFunction as RemixHeadersFunction
// Change based on runtime
} from "@remix-run/node";

// Declares global types so we don't have to import them anywhere we just consume them
export declare global { 
  declare type MetaFunction = V2_MetaFunction;
  declare type LinksFunction = RemixLinksFunction;
  declare type LoaderArgs = RemixLoaderArgs;
  declare type ActionArgs = RemixActionArgs;
  declare type DataFunctionArgs = RemixDataFunctionArgs;
  declare type HeadersFunction = RemixHeadersFunction;
  declare type ShouldRevalidateFunction = RemixShouldRevalidateFunction;
  declare type LoaderData<Loader> = ReturnType<typeof useLoaderData<Loader>>;
  declare type ActionData<Action> = ReturnType<typeof useActionData<Action>>;
}

Alright, so what is the benefit of this you might wonder? Well, now you can do the following inside your route:

export const loader = ({ request }: LoaderArgs) => {
  // fully typed request object
}

export const action = ({ request }: ActionArgs) => {
  // fully typed request object
}

// Fully typed
export const meta: V2_MetaFunction = () => {
  return [
    { title: "Very cool app | Remix" }, 
  ];
};
// you get the point, fully typed
export const shouldRevalidate: ShouldRevalidateFunction = ({ 
  currentParams,
  currentUrl,
  ...props
}) => {
  return true;
};

As you can see, everything is fully typed and requires no type imports, so let's now assume that you have moved from v1 to v2, and V2_MetaFunction is renamed to MetaFunction, what do you have to do now? Simple, you just do the following:

// As long as Remix is using React this part shouldn't change
import type { useActionData, useLoaderData, ShouldRevalidateFunction as RemixShouldRevalidateFunction } from "@remix-run/react";
// These imports will depend on the runtime you are using, if it's node you
// don't need to change anything, if it's vercel for example, change to
// from "@remix-run/vercel";
import type {
- V2_MetaFunction,
+ MetaFunction as RemixMetaFunction,
  LinksFunction as RemixLinksFunction,
  LoaderArgs as RemixLoaderArgs,
  ActionArgs as RemixActionArgs,
  DataFunctionArgs as RemixDataFunctionArgs,
  HeadersFunction as RemixHeadersFunction
// Change based on runtime
} from "@remix-run/node";

// Declares global types so we don't have to import them anywhere we just consume them
export declare global { 
- declare type MetaFunction = V2_MetaFunction;
+ declare type MetaFunction = RemixMetaFunction;
  declare type LinksFunction = RemixLinksFunction;
  declare type LoaderArgs = RemixLoaderArgs;
  declare type ActionArgs = RemixActionArgs;
  declare type DataFunctionArgs = RemixDataFunctionArgs;
  declare type HeadersFunction = RemixHeadersFunction;
  declare type ShouldRevalidateFunction = RemixShouldRevalidateFunction;
  declare type LoaderData<Loader> = ReturnType<typeof useLoaderData<Loader>>;
  declare type ActionData<Action> = ReturnType<typeof useActionData<Action>>;
}

And what if you change from node to express? Well, you just do the following:

// As long as Remix is using React this part shouldn't change
import type { useActionData, useLoaderData, ShouldRevalidateFunction as RemixShouldRevalidateFunction } from "@remix-run/react";
// These imports will depend on the runtime you are using, if it's node you
// don't need to change anything, if it's vercel for example, change to
// from "@remix-run/vercel";
import type {
  V2_MetaFunction,
  LinksFunction as RemixLinksFunction,
  LoaderArgs as RemixLoaderArgs,
  ActionArgs as RemixActionArgs,
  DataFunctionArgs as RemixDataFunctionArgs,
  HeadersFunction as RemixHeadersFunction
// Change based on runtime
- } from "@remix-run/node";
+ } from "@remix-run/express";

// Declares global types so we don't have to import them anywhere we just consume them
export declare global { 
  declare type MetaFunction = V2_MetaFunction; 
  declare type LinksFunction = RemixLinksFunction;
  declare type LoaderArgs = RemixLoaderArgs;
  declare type ActionArgs = RemixActionArgs;
  declare type DataFunctionArgs = RemixDataFunctionArgs;
  declare type HeadersFunction = RemixHeadersFunction;
  declare type ShouldRevalidateFunction = RemixShouldRevalidateFunction;
  declare type LoaderData<Loader> = ReturnType<typeof useLoaderData<Loader>>;
  declare type ActionData<Action> = ReturnType<typeof useActionData<Action>>;
}

And that is it! There are absolutely 0 changes needed in your codebase, everything has a single place of truth and you're good to go, oh, and another benefit of this? You can use this in combination with JSDoc to type your application!

I will not go into depth about how this works but you can check this repository out for examples of usage with JSDoc:

A Remix run demo in full JS with JSDoc

Thank you

If you've reached the end you're a champ! I hope you liked my article.

If you wish to support me follow me on Twitter here:

twitter.com/AlemTuzlak59192

or if you want to follow my work you can do so on GitHub:

github.com/AlemTuzlak

And you can also sign up for the newsletter to get notified whenever I publish something new!