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.
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 thekey
‘s prop type askeyof 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