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
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"
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 theLoaderFunction
type to define our loaders likeconst loader: LoaderFunction = ....
then it would not be possible to infer the return type. @tristan (great review note btw)
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>
)
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.