Advanced typings

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. 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 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 = Promise, D extends Awaited > ({ fn, key }: { fn: F, key: keyof D }) => React.JSX.Element | null In case the function returns an array: type ComponentType = Promise, D extends Awaited[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 extends Record ? K : never; async function Component Promise>(fn: F, key: KeyOf): React.JSX.Element { const item = await fn() // there is no need to check for key to be string return {item[key]}; }

Apr 9, 2025 - 01:24
 0
Advanced typings

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]}</>;
}