Practical generic patterns for React props, API clients, and data fetching — no abstract theory, just real code.
March 31, 2026

Most TypeScript generics tutorials teach you the syntax by showing you how to clone Array.map. That's fine for understanding the mechanics, but it doesn't tell you when to reach for generics in real application code. Here are the patterns that show up constantly in React apps, API clients, and data fetching layers.
The most common use case: you have an API that wraps all responses in the same shape, and you want the response type to carry the data type through.
interface ApiResponse<T> {
data: T
error: string | null
status: number
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url)
const json = await res.json()
return {
data: json.data,
error: json.error ?? null,
status: res.status,
}
}
// Usage — TypeScript knows the shape of `data`
const { data: posts } = await fetchApi<Post[]>('/api/posts')
// ^? Post[]
const { data: tenant } = await fetchApi<Tenant>('/api/tenants/arunabh-blog')
// ^? Tenant
Without the generic, you'd need a separate function for every return type or cast everything to unknown and validate manually.
When you need to work with a specific property of an object and want TypeScript to verify it exists:
function groupBy<T, K extends keyof T>(items: T[], key: K): Record<string, T[]> {
return items.reduce((acc, item) => {
const groupKey = String(item[key])
return {
...acc,
[groupKey]: [...(acc[groupKey] ?? []), item],
}
}, {} as Record<string, T[]>)
}
// TypeScript validates that 'category' exists on Post
const byCategory = groupBy(posts, 'category')
// ^^^^^^^^^^
// Error if 'category' doesn't exist on Post
// Also works with other keys
const byStatus = groupBy(posts, 'status')
const byTenant = groupBy(posts, 'tenant')
The K extends keyof T constraint means the second argument must be a valid key of whatever T is. TypeScript infers both T and K from the call site.
as PropA polymorphic component that renders as any HTML element while keeping correct prop types:
type PolymorphicProps<E extends React.ElementType> = {
as?: E
children?: React.ReactNode
} & React.ComponentPropsWithoutRef<E>
function Box<E extends React.ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as ?? 'div'
return <Component {...props}>{children}</Component>
}
// Renders as a <div> with div props
<Box className="p-4">content</Box>
// Renders as an <a> — TypeScript requires href, allows download, etc.
<Box as="a" href="/blog">Read more</Box>
// Renders as a <button> — TypeScript requires button-specific props
<Box as="button" type="submit">Submit</Box>
A hook that manages form state for any field type:
function useField<T>(initialValue: T) {
const [value, setValue] = useState<T>(initialValue)
const [error, setError] = useState<string | null>(null)
const onChange = (newValue: T) => {
setValue(newValue)
setError(null)
}
const validate = (validator: (v: T) => string | null) => {
const err = validator(value)
setError(err)
return err === null
}
return { value, error, onChange, validate }
}
// String field
const title = useField('')
// ^? { value: string; onChange: (v: string) => void; ... }
// Number field
const wordCount = useField(0)
// ^? { value: number; onChange: (v: number) => void; ... }
// Array field
const tags = useField<string[]>([])
// ^? { value: string[]; onChange: (v: string[]) => void; ... }
When you need to transform every value in an object type:
// Make every property of T optional and nullable
type Partial<T> = {
[K in keyof T]?: T[K] | null
}
// Make all string properties of T uppercase (useful for CSS variable generation)
type UppercaseStringValues<T> = {
[K in keyof T]: T[K] extends string ? Uppercase<T[K]> : T[K]
}
// Real use: transform Payload CMS field types to form field types
type FormFields<T> = {
[K in keyof T]: {
value: T[K]
error: string | null
touched: boolean
}
}
type PostFormState = FormFields<Pick<Post, 'title' | 'slug' | 'description'>>
// PostFormState = {
// title: { value: string; error: string | null; touched: boolean }
// slug: { value: string; error: string | null; touched: boolean }
// description: { value: string; error: string | null; touched: boolean }
// }
infer Keyword for Unwrapping TypesExtract the resolved value from a Promise type:
type Awaited<T> = T extends Promise<infer U> ? U : T
type PostsResult = Awaited<ReturnType<typeof getPosts>>
// ^? Post[] (unwraps the Promise<Post[]> from getPosts's return type)
This is how you derive types from existing functions without repeating yourself. If getPosts changes its return type, PostsResult updates automatically.
Generics add complexity. Don't use them when:
string | number instead of a generic T extends string | numberThe test: can you explain what the generic does in one sentence? If not, simplify.
K extends keyof T constraints verify at compile time that a key exists on an objectas props with generics give you type-safe rendering as any HTML elementinfer to extract types from Promises and other wrappers automatically