Skip to main content

Advanced: Generic Component Props with Constraints

Advanced generic constraints allow React components to accept flexible but strictly typed parameters. By constraining generics with extends, you ensure that only compatible types are used—for example, a list component that accepts items of any type but requires each item to have an id property. This pattern is foundational for building reusable component libraries that scale without sacrificing type safety.

Generic Type Constraints with Extends

The extends keyword constrains what types a generic can accept. For example, a component might accept any object type that has specific properties:

// List.tsx
interface HasId {
id: string | number;
}

interface ListProps<T extends HasId> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor?: (item: T) => string | number;
}

const List = <T extends HasId>({ items, renderItem, keyExtractor }: ListProps<T>) => (
<ul>
{items.map((item, index) => (
<li key={keyExtractor ? keyExtractor(item) : item.id}>
{renderItem(item, index)}
</li>
))}
</ul>
);

export default List;

The <T extends HasId> constraint ensures T must have an id property. Inside the component, you can safely access item.id without type errors. Callers must pass items that have id:

interface User {
id: number;
name: string;
}

interface Product {
id: string;
title: string;
price: number;
}

// Valid: User has id
<List<User>
items={[{ id: 1, name: 'Alice' }]}
renderItem={(user) => <span>{user.name}</span>}
/>

// Valid: Product has id
<List<Product>
items={[{ id: 'p1', title: 'Laptop', price: 999 }]}
renderItem={(product) => <span>{product.title}</span>}
/>

// Error: Item doesn't have id property
interface BadItem {
name: string;
}
<List<BadItem>
items={[{ name: 'Bad' }]}
renderItem={(item) => <span>{item.name}</span>}
/>

TypeScript enforces the constraint at compile time.

Constraining Generics to Specific Types

Constrain a generic to a specific category of types, like objects, functions, or component types:

// FilterableList.tsx
interface FilterableListProps<T extends object> {
items: T[];
filterKey: keyof T; // Must be a property of T
filterValue: T[keyof T]; // Must be a value type of T
children: (item: T) => React.ReactNode;
}

const FilterableList = <T extends object>({
items,
filterKey,
filterValue,
children,
}: FilterableListProps<T>) => {
const filtered = items.filter((item) => item[filterKey] === filterValue);

return (
<ul>
{filtered.map((item, i) => (
<li key={i}>{children(item)}</li>
))}
</ul>
);
};

export default FilterableList;

// Usage
interface Post {
id: number;
title: string;
published: boolean;
}

// filterKey must be a key of Post, filterValue must match the value type
<FilterableList<Post>
items={[
{ id: 1, title: 'Hello', published: true },
{ id: 2, title: 'Draft', published: false },
]}
filterKey="published"
filterValue={true}
>
{(post) => <span>{post.title}</span>}
</FilterableList>

The generic is constrained to extends object, and keyof T and T[keyof T] ensure type safety on object properties.

Multiple Constraints

Use multiple constraints with & to require multiple properties or behaviors:

// Validator.tsx
interface Identifiable {
id: string | number;
}

interface Comparable {
compareTo(other: this): number;
}

type Validatable = Identifiable & Comparable;

interface ValidatorProps<T extends Validatable> {
items: T[];
onValidate?: (item: T) => boolean;
}

const Validator = <T extends Validatable>({ items, onValidate }: ValidatorProps<T>) => (
<ul>
{items
.filter((item) => (onValidate ? onValidate(item) : true))
.sort((a, b) => a.compareTo(b))
.map((item) => (
<li key={item.id}>{item.id}</li>
))}
</ul>
);

export default Validator;

The constraint <T extends Validatable> means T must have both id and compareTo properties.

Conditional Types for Smart Props

Conditional types (T extends U ? X : Y) let you choose the type based on a condition:

// DataDisplay.tsx
type ExtractArrayElement<T> = T extends (infer U)[] ? U : T;

interface DataDisplayProps<T> {
data: T;
renderItem?: (item: ExtractArrayElement<T>) => React.ReactNode;
render?: (data: T) => React.ReactNode;
}

const DataDisplay = <T,>({ data, renderItem, render }: DataDisplayProps<T>) => {
if (Array.isArray(data)) {
return (
<ul>
{data.map((item, i) => (
<li key={i}>{renderItem ? renderItem(item) : String(item)}</li>
))}
</ul>
);
}

return <div>{render ? render(data) : String(data)}</div>;
};

export default DataDisplay;

// Usage
// For arrays, renderItem is available
<DataDisplay<number[]>
data={[1, 2, 3]}
renderItem={(num) => <span>{num}</span>}
/>

// For non-arrays, render is available
<DataDisplay<string>
data="Hello"
render={(str) => <p>{str}</p>}
/>

The conditional type automatically adjusts the props based on whether T is an array.

Default Generic Parameters

Provide defaults for generic parameters to make them optional:

// Paginated.tsx
interface PaginatedProps<T = any> {
items: T[];
itemsPerPage?: number;
renderItem: (item: T) => React.ReactNode;
}

const Paginated = <T = any>({
items,
itemsPerPage = 10,
renderItem,
}: PaginatedProps<T>) => {
const [page, setPage] = React.useState(0);
const start = page * itemsPerPage;
const paged = items.slice(start, start + itemsPerPage);

return (
<div>
<ul>
{paged.map((item, i) => (
<li key={i}>{renderItem(item)}</li>
))}
</ul>
<div>
<button onClick={() => setPage(page - 1)} disabled={page === 0}>
Prev
</button>
<button
onClick={() => setPage(page + 1)}
disabled={start + itemsPerPage >= items.length}
>
Next
</button>
</div>
</div>
);
};

export default Paginated;

// Usage without specifying the generic (uses 'any' default)
<Paginated items={['a', 'b', 'c']} renderItem={(item) => <span>{item}</span>} />

// Or explicitly specify the type
<Paginated<string>
items={['a', 'b', 'c']}
renderItem={(item) => <span>{item}</span>}
/>

Default parameters make generics easier to use when you don't need strict typing.

Extracting Generic Types from Props

Use the infer keyword to extract types from complex prop structures:

// FormField.tsx
type ExtractFormValue<T> = T extends { value: infer V } ? V : never;

interface GenericFormFieldProps<T extends { value: any }> {
value: T['value'];
onChange: (value: T['value']) => void;
label: string;
}

const FormField = <T extends { value: any }>({
value,
onChange,
label,
}: GenericFormFieldProps<T>) => (
<label>
{label}
<input
type="text"
value={String(value)}
onChange={(e) => onChange(e.target.value as ExtractFormValue<T>)}
/>
</label>
);

export default FormField;

Comparison Table: Generic Patterns

PatternSyntaxUse Case
Simple generic<T>Flexible component accepting any type
Single constraint<T extends Type>Type must have specific property or behavior
Multiple constraints<T extends A & B>Type must satisfy multiple requirements
ConditionalT extends U ? X : YChoose type based on a condition
Infer typeT extends (infer U)[]Extract inner type from array or other structure
Default parameter<T = DefaultType>Provide a fallback when not specified

Key Takeaways

  • Constrain generics with extends to ensure only compatible types are passed, preventing invalid usage.
  • Use keyof T to refer to property keys of a type, and T[K] to access value types for those properties.
  • Multiple constraints with & enforce that a type must satisfy multiple requirements.
  • Conditional types intelligently adjust types based on conditions, enabling smart prop inference.
  • The infer keyword extracts inner types from complex structures, useful for array elements or value types.
  • Default generic parameters make components easier to use when strict typing is not needed.

Frequently Asked Questions

What's the difference between <T extends Type> and <T = Type>?

extends Type is a constraint—it restricts what types T can be. = Type is a default—it's used when T is not explicitly provided. A constraint is enforced; a default is a fallback.

Can I have multiple generic parameters?

Yes—<T, U, V> declares three generics. Each can have its own constraints: <T extends Identifiable, U extends T> means U must extend T, creating a hierarchy.

How do I extract the type of an array element?

Use T extends (infer U)[] to extract U as the array element type. Inside the conditional, U is the element type.

What's the difference between <T> and <T = any>?

<T> requires the type to be explicitly provided (or inferred from usage). <T = any> provides a default fallback if not specified. For components used in many contexts, defaults make them easier to use.

Can I constrain a generic to a React component type?

Yes—<T extends React.ComponentType<any>> or <T extends React.FC<any>> constrains T to be a component. You can further constrain the props with <T extends React.FC<{ required: string }>>.

Further Reading