Advanced typings



This content originally appeared on DEV Community and was authored by Cristian Torres

Lets assume you’re trying to create a generic component that receives a data-fetching function, lets call it fn, which returns an item or an array of some type and another prop, lets call it key, with one of the keys of the function’s return type.

In order to get autocompletion for the key prop.
Auto completion

The data-fetching function

async function getData() {
  return {key1: "b", key2: 4, key3: false};
}

The Question

How to declare the type of another component’s prop to accept only the keys of the data type returned by the data-fetching function.

The Process

You could “easily” type the props as follows:

type ReturnedType = {
  key1: string
  key2: number
  key3: boolean
}
type ComponentProps = {
  fn: () => Promise<ReturnedType>
  key: "key1" | "key2" | "key3"
}

But this doesn’t really scale as the component is not generic, meaning the data-fetching function can only be one that returns an object with the shape of ReturnedType.

Also, every time the type ReturnedType changes, we need to make changes to the type of the key prop.

Note
We could also declare the key‘s prop type as keyof ReturnedType.

The Fix: keyof

Instead of defining the props type, we will define our component’s type and use keyof to define the type of key prop, resulting in the type being the union of keys of the awaited response of the function we will be passing on fn.

TL;DR:

type ComponentType = <
  F extends () => Promise<Record<string, unknown>>, 
  D extends Awaited<ReturnType<F>>
> ({
  fn,
  key
}: {
  fn: F,
  key: keyof D
}) => React.JSX.Element | null

In case the function returns an array:

type ComponentType = <
  F extends () => Promise<Record<string, unknown>>, 
  D extends Awaited<ReturnType<F>>[number]
> ({
  fn,
  key
}: {
  fn: F,
  key: keyof D
}) => React.JSX.Element | null

In this case we need to check for the type of key prop to be “string”:

// Server component
const Component: ComponentType = async ({fn, key}) => {
  const item = await fn();
  if (typeof key !== "string") return null;
  return <>{item[key]}</>
}
// Client component
const Component: ComponentType = ({fn, key}) => {
  const [item, setItem] = useState();
  useEffect(() => {
    fn().then(setItem)
  }, [])
  if (typeof key !== "string") return null;
  return <>{item[key]}</>
}

Another way to do it (with a function returning an array and using a inferring helper):

type KeyOf<T> = T extends Record<infer K extends string, unknown> ? K : never;
async function Component<F extends () => Promise<Record<string, unknown>[]>>(fn: F, key: KeyOf<Awaited<ReturnType<F>>[number]>): React.JSX.Element {
  const item = await fn()
  // there is no need to check for key to be string
  return <>{item[key]}</>;
}


This content originally appeared on DEV Community and was authored by Cristian Torres