Web DevelopmentTypeScriptGenericsReactType Safety

TypeScript Generics You Actually Use in Real Projects

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

March 31, 2026

typescript generics you actually use

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.

Generic API Response Wrapper

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.

Constrained Generics for Object Keys

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.

Generic React Components with as Prop

A 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>

Generic Form Field Hook

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; ... }

Mapped Types for API Transformations

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

The infer Keyword for Unwrapping Types

Extract 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.

When Not to Use Generics

Generics add complexity. Don't use them when:

  • A union type is clearer: string | number instead of a generic T extends string | number
  • The function only works with one type in practice
  • You'd need to add 3+ constraints to make the generic actually safe

The test: can you explain what the generic does in one sentence? If not, simplify.

Key Takeaways

  • Generic API wrappers let a single fetch function serve every endpoint with full type safety
  • K extends keyof T constraints verify at compile time that a key exists on an object
  • Polymorphic as props with generics give you type-safe rendering as any HTML element
  • Mapped types transform entire object types without repeating each property
  • Use infer to extract types from Promises and other wrappers automatically