You might not be using typeof as much as you could

Here I show a handy little trick using the typeof operator in TypeScript to reduce the number of concrete types one might have to create

You might not be using typeof as much as you could

In JavaScript, the typeof operator can be used in an expression context to return the type.

For example:

console.log(typeof "i'm a string") // prints "string"

And in addition, in TypeScript, we can use the typeof operator to define a type.

For example:

type T = typeof "i'm a string' // T is the type "T: string"
๐Ÿ”Ž
Read more about this over at the TypeScript docs

With the basics out of the way, let's dive into a more complex example of where I use the typeof operator to define a type that I may have customarily defined as a concrete type.

In Remix, a route (aka page) is typically accompanied by a loader function which fetches the data that is generally rendered on the page.

Here's an example that loads a couple of cat breeds and cat facts using two awesome open APIs by https://catfact.ninja.

export const loader = async ({ request }: DataFunctionArgs) => {
  const catBreeds: { data: { breed: string; coat: string }[] } = await (
    await fetch(`https://catfact.ninja/breeds?limit=${tableSize}`)
  ).json()
  const catFacts: { data: { fact: string; length: number }[] } = await (
    await fetch(`https://catfact.ninja/facts?limit=${tableSize}`)
  ).json()

  return catBreeds.data.map((cb, i) => ({
    breed: cb.breed,
    coat: cb.coat,
    fact: catFacts.data[i].fact,
  }))
}

The type for this loader function is implicitly (({ request }: DataFunctionArgs)) => Promise<{breed: string, coat: string, fact: string}[]>

When I go to use/reference it, I might be tempted to create the concrete type to represent the returned shape.

For example:

type CatItem = {
  breed: string
  coat: string
  fact: string
}

const MyComp = () => {
  const data = useLoaderData<CatItem[]>()
  return <div>{data.map(c => ...)</div>
}

But I don't have to. If I wanted to, I can avoid the explicit CatItem type definition, and infer it from the return type, like so Awaited<ReturnType<typeof loader>> , but more on this later.

Due to the how the internals of the useLoaderData hook work, I can simply reference it by using the typeof operator. Functionally, the typeof operator gives me the same outcome.

For example:

const MyComp = () => {
  const data = useLoaderData<typeof loader>()
  return <div>{data.map(c => ...)</div>
}
Itโ€™s important to note that this only works because we havenโ€™t defined an explicit return type for our loader function which allows typescript to infer the type based on the structure of the object we return. If we were to follow an earlier remix convention of using the LoaderFunction type to define our loaders like const loader: LoaderFunction = .... then it would not be possible to infer the return type. @tristan (great review note btw)
๐Ÿ”ฌ
See it in action on Stackblitz

Now I know what you're thinking. Sure, this is fine for the simple use case, but what about when it gets more complex? What about when one has to pass elements from the collection to another react component?

Well, using a few built-in type helpers, I can dig the type out of the loader function, like so type CatItem = Awaited<ReturnType<typeof loader>>[number]

This means I can easily refactor the cat section to a new component.

For example:

type CatItem = Awaited<ReturnType<typeof loader>>[number]

const CatRow = ({ catItem }: { catItem: CatItem }) => (
  <section>
    <h2>{catItem.breed}</h2>
    <dl>
      <dt><b>Coat</b></dt>
      <dd>{catItem.coat}</dd>
      <dt><b>Fact</b></dt>
      <dd>{catItem.fact}</dd>
    </dl>
  </section>
)
๐Ÿ”ฌ
See it in action on Stackblitz

The great thing about building and defining types like this is that if I were to map more properties in the loader function, those properties would automatically flow through to the type ๐ŸŽ‰

Next time you reach to type out a type, stop and think whether you can make your life easier by using the typeof operator.

Happy coding.