Skip to main content

Advanced Component Prop Inference Patterns

Component prop inference extracts prop types from existing components automatically, allowing you to build type-safe wrappers and derived components without manually duplicating prop interfaces. Using TypeScript's typeof operator, React.ComponentProps, conditional type inference with infer, and generic parameter defaults, you can create components that automatically adapt to their wrapped component's interface and enable consumers to discover available props through IDE autocomplete without maintaining separate type definitions.

Extracting Props from Components

The simplest form of prop inference uses React.ComponentProps<typeof Component> to extract a component's prop type:

import React from "react";

// Define a component with specific props
interface ButtonProps {
variant: "primary" | "secondary";
size: "small" | "large";
onClick: () => void;
children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({
variant,
size,
onClick,
children,
}) => (
<button
className={`btn-${variant} btn-${size}`}
onClick={onClick}
>
{children}
</button>
);

// Extract Button's props type automatically
type ButtonProps2 = React.ComponentProps<typeof Button>;
// Result: { variant: "primary" | "secondary"; size: "small" | "large"; ... }

// Build a wrapper that extends Button's props
interface EnhancedButtonProps
extends React.ComponentProps<typeof Button> {
tooltip?: string;
disabled?: boolean;
}

const EnhancedButton: React.FC<EnhancedButtonProps> = ({
tooltip,
disabled,
...restProps
}) => (
<div title={tooltip}>
<Button {...restProps} onClick={disabled ? () => {} : restProps.onClick} />
</div>
);

// EnhancedButton requires all original Button props plus optional tooltip/disabled
<EnhancedButton
variant="primary"
size="large"
onClick={() => console.log("clicked")}
tooltip="Save your changes"
>
Save
</EnhancedButton>;

This pattern keeps prop definitions synchronized automatically: if you update Button's interface, any code using React.ComponentProps<typeof Button> reflects the change without modification.

Conditional Inference for Generic Components

For generic components, infer type parameters to avoid explicit type annotations:

// A generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}

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

// Extract the item type from a List component's items prop
type InferItemType<T> = T extends ListProps<infer U> ? U : never;

interface UserListProps extends ListProps<User> {
onSelectUser?: (user: User) => void;
}

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

const UserList: React.FC<UserListProps> = ({
items,
renderItem,
keyExtractor,
onSelectUser,
}) => {
const handleSelect = (user: InferItemType<UserListProps>) => {
// user is inferred as User from the component's props
onSelectUser?.(user);
};

return (
<List
items={items}
renderItem={(item) => (
<div onClick={() => handleSelect(item)}>
{renderItem(item, 0)}
</div>
)}
keyExtractor={keyExtractor}
/>
);
};

Building a Generic Wrapper with Full Type Inference

Create a wrapper component that fully infers the wrapped component's prop type:

// A higher-order component that preserves and infers all prop types
const withDataFetching = <P extends object>(
Component: React.ComponentType<P>,
fetcher: (props: P) => Promise<any>
): React.FC<P> => {
return (props: P) => {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);

React.useEffect(() => {
setLoading(true);
fetcher(props)
.then(() => setError(null))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [props]);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return <Component {...props} />;
};
};

interface UserDetailProps {
userId: string;
showEmail: boolean;
}

const UserDetail: React.FC<UserDetailProps> = ({
userId,
showEmail,
}) => (
<div>
User: {userId} {showEmail && "(Email shown)"}
</div>
);

const UserDetailWithData = withDataFetching(UserDetail, async (props) => {
// Fetch user data based on props
console.log(`Fetching user ${props.userId}`);
});

// UserDetailWithData has the exact same prop type as UserDetail
<UserDetailWithData userId="123" showEmail={true} />;

Advanced: Extracting Specific Props

Sometimes you only want certain props from a component. Create a utility that extracts specific properties:

// Extract only specific keys from a component's props
type ExtractKeys<P, K extends keyof P> = Pick<P, K>;

interface FormProps {
onSubmit: (data: any) => void;
initialValues?: Record<string, any>;
validation?: (data: any) => Record<string, string>;
children: React.ReactNode;
}

const Form: React.FC<FormProps> = ({
onSubmit,
initialValues,
validation,
children,
}) => (
<form onSubmit={(e) => {
e.preventDefault();
onSubmit({});
}}>
{children}
</form>
);

// Extract only the callback props from Form
type FormCallbacks = ExtractKeys<
React.ComponentProps<typeof Form>,
"onSubmit" | "validation"
>;

// Use in a context to provide form behavior
interface FormContextValue extends FormCallbacks {
values: Record<string, any>;
}

const FormContext = React.createContext<FormContextValue | null>(null);

const useFormContext = () => {
const ctx = React.useContext(FormContext);
if (!ctx) throw new Error("useFormContext must be used inside FormProvider");
return ctx;
};

Prop Inference Patterns Table

PatternPurposeExample
React.ComponentProps<typeof C>Extract all propsGet prop type from component
T extends Component<infer P>Infer from genericExtract generic parameter
Pick<Props, "key1" | "key2">Select specific propsExtract callbacks only
Omit<Props, "key">Remove specific propsRemove internal props
keyof React.ComponentProps<C>Get all available keysIterate over props

Key Takeaways

  • Use React.ComponentProps<typeof Component> to extract prop types automatically, keeping wrappers synchronized with changes to the original component.
  • Inference with infer in conditional types allows you to extract generic type parameters from component interfaces.
  • Building wrappers with full prop inference eliminates boilerplate and makes refactoring safer: prop changes propagate automatically.
  • Extract specific props with Pick and Omit to create focused interfaces for specialized use cases (callbacks, data, etc.).
  • Prop inference is particularly powerful for library components where you want wrappers to adapt to any component without manual configuration.

Frequently Asked Questions

Why does TypeScript sometimes lose inference in component wrappers?

TypeScript's inference is context-sensitive. If you pass any implicitly or don't provide enough type information, inference fails. Always explicitly annotate generic parameters or provide concrete values: <P extends object> with actual type arguments gives the compiler more information.

How do I infer from a component's defaultProps?

React.ComponentProps automatically includes default props. If a component sets defaultProps, those properties become optional in the inferred type, matching the component's actual API.

Can I infer the return type of a component's render function?

Yes, use React.ReactNode or JSX.Element for the return type. To infer from a specific component, write ReturnType<typeof Component> to get what the component function returns (a React element or null).

When should I use inferred types vs. explicit interfaces?

Infer types for wrappers and derived components that depend on existing components. Use explicit interfaces for public API components (library components) where you want to control and document the contract. Mix both: explicit interfaces for primary components, inferred types for wrappers.

Further Reading