React with TypeScript Integration
1. Component Props TypeScript Interfaces and Types
| Pattern | Syntax | Use Case | Example |
|---|---|---|---|
| Basic Interface | interface Props { ... } |
Define component prop types | Simple props with required/optional fields |
| Type Alias | type Props = { ... } |
Alternative to interface, unions, intersections | When needing union types or primitives |
| Optional Props | prop?: string |
Props that may not be provided | Default values, conditional rendering |
| Default Props | Destructure with defaults | Provide fallback values | { size = 'medium' } |
| Children Prop | children: React.ReactNode |
Type children elements | Wrapper components, layouts |
| Generic Props | interface Props<T> { ... } |
Type-safe generic components | Lists, tables, data displays |
| Discriminated Unions | type Props = A | B |
Variant props based on type field | Button variants, conditional props |
Example: Component props interfaces
// Basic interface
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary' | 'danger';
}
function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
// Children prop
interface CardProps {
title: string;
children: React.ReactNode;
footer?: React.ReactNode;
}
function Card({ title, children, footer }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Generic props
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage with type inference
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
Example: Discriminated unions for variant props
// Button with discriminated union
type BaseButtonProps = {
label: string;
disabled?: boolean;
};
type PrimaryButtonProps = BaseButtonProps & {
variant: 'primary';
color: 'blue' | 'green' | 'red';
};
type SecondaryButtonProps = BaseButtonProps & {
variant: 'secondary';
outlined: boolean;
};
type LinkButtonProps = BaseButtonProps & {
variant: 'link';
href: string;
target?: '_blank' | '_self';
};
type ButtonProps = PrimaryButtonProps | SecondaryButtonProps | LinkButtonProps;
function Button(props: ButtonProps) {
const { variant, label, disabled } = props;
if (variant === 'primary') {
// TypeScript knows props.color exists
return <button className={`btn-${props.color}`}>{label}</button>;
}
if (variant === 'secondary') {
// TypeScript knows props.outlined exists
return <button className={props.outlined ? 'outlined' : ''}>{label}</button>;
}
// TypeScript knows props.href exists
return <a href={props.href} target={props.target}>{label}</a>;
}
// Complex discriminated union
type FormFieldProps =
| { type: 'text'; maxLength?: number; pattern?: string }
| { type: 'number'; min?: number; max?: number; step?: number }
| { type: 'select'; options: Array<{ label: string; value: string }> }
| { type: 'checkbox'; checked: boolean };
function FormField(props: FormFieldProps & { name: string; label: string }) {
switch (props.type) {
case 'text':
return <input type="text" maxLength={props.maxLength} pattern={props.pattern} />;
case 'number':
return <input type="number" min={props.min} max={props.max} step={props.step} />;
case 'select':
return (
<select>
{props.options.map(opt => <option value={opt.value}>{opt.label}</option>)}
</select>
);
case 'checkbox':
return <input type="checkbox" checked={props.checked} />;
}
}
2. Hook Type Definitions and Generic Hooks (useState)
| Hook | Type Syntax | Type Inference | Example |
|---|---|---|---|
| useState | useState<Type>(initial) |
Auto-infers from initial value | const [count, setCount] = useState(0) |
| useRef | useRef<HTMLElement | null>(null) |
Must specify element type | const ref = useRef<HTMLDivElement>(null) |
| useReducer | useReducer<Reducer<State, Action>> |
Type state and action | Typed reducer with discriminated unions |
| useContext | useContext<ContextType>(Context) |
Infers from context creation | Type-safe context consumption |
| useMemo | useMemo<ReturnType>(() => ...) |
Auto-infers return type | Type-safe memoization |
| useCallback | useCallback<FunctionType>(...) |
Infers function signature | Type-safe callbacks |
| Custom Hooks | function useHook<T>(): ReturnType |
Explicit return type | Generic reusable hooks |
Example: Typed React hooks
// useState with explicit type
const [user, setUser] = useState<User | null>(null);
const [count, setCount] = useState(0); // inferred as number
// useState with complex types
interface FormState {
email: string;
password: string;
errors: Record<string, string>;
}
const [form, setForm] = useState<FormState>({
email: '',
password: '',
errors: {},
});
// useRef with DOM elements
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus(); // TypeScript knows .focus() exists
}
}, []);
// useRef for mutable values
const intervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
console.log('tick');
}, 1000);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
// useReducer with typed actions
type State = { count: number; lastAction: string };
type Action =
| { type: 'increment'; payload?: number }
| { type: 'decrement'; payload?: number }
| { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return {
count: state.count + (action.payload ?? 1),
lastAction: 'increment'
};
case 'decrement':
return {
count: state.count - (action.payload ?? 1),
lastAction: 'decrement'
};
case 'reset':
return { count: 0, lastAction: 'reset' };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, lastAction: 'init' });
// TypeScript validates action types
dispatch({ type: 'increment', payload: 5 }); // ✅
dispatch({ type: 'invalid' }); // ❌ Error
Example: Generic custom hooks
// Generic fetch hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json as T);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err : new Error('Unknown error'));
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage with type inference
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
// TypeScript knows user has id, name, email
return <div>{user.name} - {user.email}</div>;
}
// Generic array hook with constraints
function useArray<T>(initialValue: T[] = []) {
const [array, setArray] = useState<T[]>(initialValue);
const push = useCallback((item: T) => {
setArray(prev => [...prev, item]);
}, []);
const remove = useCallback((index: number) => {
setArray(prev => prev.filter((_, i) => i !== index));
}, []);
const update = useCallback((index: number, item: T) => {
setArray(prev => prev.map((val, i) => i === index ? item : val));
}, []);
const clear = useCallback(() => {
setArray([]);
}, []);
return { array, set: setArray, push, remove, update, clear };
}
// Usage
const { array: todos, push, remove } = useArray<Todo>([]);
// Generic storage hook
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
3. Event Handler Type Safety (React.MouseEvent, React.ChangeEvent)
| Event Type | React Type | Native Type | Common Usage |
|---|---|---|---|
| Click | React.MouseEvent<HTMLElement> |
MouseEvent |
Button clicks, div clicks |
| Change | React.ChangeEvent<HTMLInputElement> |
Event |
Input, textarea, select changes |
| Submit | React.FormEvent<HTMLFormElement> |
Event |
Form submissions |
| Focus | React.FocusEvent<HTMLElement> |
FocusEvent |
Input focus/blur |
| Keyboard | React.KeyboardEvent<HTMLElement> |
KeyboardEvent |
Key press, key down, key up |
| Drag | React.DragEvent<HTMLElement> |
DragEvent |
Drag and drop interactions |
Example: Type-safe event handlers
// Click events
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
console.log('Button clicked', event.currentTarget.name);
}
// Different element types
function handleDivClick(event: React.MouseEvent<HTMLDivElement>) {
console.log('Div clicked at', event.clientX, event.clientY);
}
// Input change events
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value;
const name = event.target.name;
console.log(`${name}: ${value}`);
}
// Select change events
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const selectedValue = event.target.value;
const selectedIndex = event.target.selectedIndex;
console.log(`Selected: ${selectedValue} at index ${selectedIndex}`);
}
// Form submission
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const data = Object.fromEntries(formData);
console.log('Form data:', data);
}
// Keyboard events
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
console.log('Enter pressed');
}
// Check modifiers
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Ctrl+S pressed');
}
}
// Complete form component
interface FormData {
email: string;
password: string;
}
function LoginForm() {
const [formData, setFormData] = useState<FormData>({
email: '',
password: '',
});
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Submitting:', formData);
};
return (
<form onSubmit={handleFormSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
/>
<button type="submit">Login</button>
</form>
);
}
Example: Generic event handler types
// Reusable event handler types
type ClickHandler = React.MouseEvent<HTMLButtonElement>;
type InputChangeHandler = React.ChangeEvent<HTMLInputElement>;
type FormSubmitHandler = React.FormEvent<HTMLFormElement>;
interface ButtonProps {
onClick: (event: ClickHandler) => void;
label: string;
}
function Button({ onClick, label }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
// Generic handler wrapper
type ElementClickHandler<T extends HTMLElement> = (
event: React.MouseEvent<T>
) => void;
interface ClickableProps<T extends HTMLElement> {
onClick: ElementClickHandler<T>;
children: React.ReactNode;
}
// Event handler with custom parameters
interface SearchInputProps {
onSearch: (query: string) => void;
onClear: () => void;
}
function SearchInput({ onSearch, onClear }: SearchInputProps) {
const [query, setQuery] = useState('');
const handleChange: InputChangeHandler = (event) => {
setQuery(event.target.value);
};
const handleSubmit: FormSubmitHandler = (event) => {
event.preventDefault();
onSearch(query); // Call with typed parameter
};
const handleClear: ClickHandler = (event) => {
event.preventDefault();
setQuery('');
onClear();
};
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={handleChange} />
<button type="submit">Search</button>
<button type="button" onClick={handleClear}>Clear</button>
</form>
);
}
4. Context API TypeScript Patterns and Type Inference
| Pattern | Implementation | Benefits | Use Case |
|---|---|---|---|
| Typed Context | createContext<Type | null>(null) |
Type safety for context value | All context implementations |
| Context with Hook | Custom hook that throws if outside provider | Guaranteed non-null context, better DX | Enforcing provider usage |
| Generic Context | createContext<T> with generic |
Reusable context pattern | Generic state containers |
| Context with Actions | Separate state and dispatch | Type-safe actions, better organization | Complex state management |
| Multiple Contexts | Compose multiple providers | Separation of concerns | Large apps with different domains |
Example: Type-safe context with custom hook
// Define context type
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isAuthenticated: boolean;
}
// Create context with null default
const AuthContext = createContext<AuthContextType | null>(null);
// Provider component
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
// API call
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
const userData = await response.json();
setUser(userData);
};
const logout = () => {
setUser(null);
};
const value: AuthContextType = {
user,
login,
logout,
isAuthenticated: user !== null,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Custom hook with type guard
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage - TypeScript knows context is never null
function Profile() {
const { user, logout } = useAuth(); // No null check needed
return (
<div>
<h1>{user?.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
Example: Context with reducer pattern
// State and actions
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
}
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: string }
| { type: 'DELETE_TODO'; id: string }
| { type: 'SET_FILTER'; filter: TodoState['filter'] };
// Reducer
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{ id: Date.now().toString(), text: action.text, completed: false },
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id),
};
case 'SET_FILTER':
return { ...state, filter: action.filter };
default:
return state;
}
}
// Context types
interface TodoContextType {
state: TodoState;
dispatch: React.Dispatch<TodoAction>;
}
const TodoContext = createContext<TodoContextType | null>(null);
// Provider
export function TodoProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
});
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// Custom hooks
export function useTodos() {
const context = useContext(TodoContext);
if (!context) throw new Error('useTodos must be used within TodoProvider');
return context;
}
// Helper hooks
export function useTodoActions() {
const { dispatch } = useTodos();
return {
addTodo: (text: string) => dispatch({ type: 'ADD_TODO', text }),
toggleTodo: (id: string) => dispatch({ type: 'TOGGLE_TODO', id }),
deleteTodo: (id: string) => dispatch({ type: 'DELETE_TODO', id }),
setFilter: (filter: TodoState['filter']) => dispatch({ type: 'SET_FILTER', filter }),
};
}
// Usage
function TodoList() {
const { state } = useTodos();
const { toggleTodo, deleteTodo } = useTodoActions();
return (
<ul>
{state.todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
5. Custom Hook Type Safety and Return Types
| Concept | Pattern | Benefit | Example |
|---|---|---|---|
| Return Type Inference | Let TypeScript infer return type | Less verbose, auto-updates | Hook without explicit return type |
| Explicit Return Type | Define return type explicitly | Better documentation, API contracts | Public library hooks |
| Tuple Return | return [value, setter] as const |
Array destructuring with types | useState-like hooks |
| Generic Hooks | function useHook<T>() |
Reusable with different types | Generic data fetching, storage |
| Conditional Types | Return type based on parameters | Smart type inference | Optional parameters affecting return |
| Type Guards | Runtime checks with type narrowing | Type safety with validation | Parsing external data |
Example: Custom hook with type inference
// Inferred return type
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount(c => c + 1), []);
const decrement = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
// Return type inferred as { count: number; increment: () => void; ... }
return { count, increment, decrement, reset };
}
// Tuple return with const assertion
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
// 'as const' makes it a tuple, not array
return [value, toggle] as const;
}
// Usage with proper types
const [isOpen, toggleOpen] = useToggle(); // isOpen: boolean, toggleOpen: () => void
// Generic hook with constraints
function useAsync<T, E = Error>(
asyncFunction: () => Promise<T>,
immediate = true
) {
const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<E | null>(null);
const execute = useCallback(async () => {
setStatus('pending');
setData(null);
setError(null);
try {
const response = await asyncFunction();
setData(response);
setStatus('success');
return response;
} catch (err) {
setError(err as E);
setStatus('error');
throw err;
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, data, error };
}
// Usage with type inference
interface User {
id: number;
name: string;
}
function UserProfile() {
const { data: user, status } = useAsync<User>(
() => fetch('/api/user').then(r => r.json()),
true
);
// TypeScript knows user is User | null
if (status === 'success' && user) {
return <div>{user.name}</div>;
}
return <div>Loading...</div>;
}
Example: Advanced generic hooks
// Generic form hook
interface UseFormOptions<T> {
initialValues: T;
onSubmit: (values: T) => void | Promise<void>;
validate?: (values: T) => Partial<Record<keyof T, string>>;
}
function useForm<T extends Record<string, any>>({
initialValues,
onSubmit,
validate,
}: UseFormOptions<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = useCallback((name: keyof T, value: any) => {
setValues(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: undefined }));
}, []);
const handleSubmit = useCallback(async (event?: React.FormEvent) => {
event?.preventDefault();
if (validate) {
const validationErrors = validate(values);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
}
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
}, [values, validate, onSubmit]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, [initialValues]);
return { values, errors, isSubmitting, handleChange, handleSubmit, reset };
}
// Usage with type inference
interface LoginForm {
email: string;
password: string;
}
function Login() {
const { values, errors, handleChange, handleSubmit } = useForm<LoginForm>({
initialValues: { email: '', password: '' },
onSubmit: async (values) => {
await login(values.email, values.password);
},
validate: (values) => {
const errors: Partial<Record<keyof LoginForm, string>> = {};
if (!values.email) errors.email = 'Required';
if (!values.password) errors.password = 'Required';
return errors;
},
});
// TypeScript knows values.email and values.password exist
return (
<form onSubmit={handleSubmit}>
<input
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
<input
type="password"
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
/>
{errors.password && <span>{errors.password}</span>}
<button type="submit">Login</button>
</form>
);
}
6. React.FC vs Function Component Types - Best Practices
| Approach | Syntax | Pros | Cons |
|---|---|---|---|
| React.FC | const Comp: React.FC<Props> = ... |
Includes children type, shorter syntax | Implicit children (not always wanted), verbose |
| Function Declaration | function Comp(props: Props) {...} |
Explicit, clear, standard TypeScript | Need to define children explicitly |
| Arrow Function | const Comp = (props: Props) => ... |
Concise, explicit types | No hoisting, slightly more verbose |
| With Generics | function Comp<T>(props: Props<T>) {...} |
Type-safe generic components | More complex, requires understanding generics |
Example: React.FC vs function declaration
// ❌ React.FC (less recommended now)
interface ButtonProps {
label: string;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ label, onClick, children }) => {
// children is implicitly included (might not be wanted)
return <button onClick={onClick}>{label}{children}</button>;
};
// ✅ Function declaration (recommended)
interface CardProps {
title: string;
children: React.ReactNode; // Explicit children
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
// ✅ Arrow function (also good)
interface AlertProps {
message: string;
type: 'info' | 'warning' | 'error';
}
const Alert = ({ message, type }: AlertProps) => {
return <div className={`alert alert-${type}`}>{message}</div>;
};
// ✅ Function with explicit return type
function UserCard({ user }: { user: User }): JSX.Element {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// Generic component - function declaration
function List<T>({ items, renderItem }: {
items: T[];
renderItem: (item: T) => React.ReactNode;
}) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// Component with default props (function declaration)
interface BadgeProps {
text: string;
color?: 'red' | 'blue' | 'green';
size?: 'small' | 'medium' | 'large';
}
function Badge({ text, color = 'blue', size = 'medium' }: BadgeProps) {
return <span className={`badge badge-${color} badge-${size}`}>{text}</span>;
}
Example: Component type patterns
// Polymorphic component (advanced)
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>['ref'];
// Usage
interface TextProps {
color?: 'primary' | 'secondary';
}
function Text<C extends React.ElementType = 'span'>({
as,
color = 'primary',
children,
...props
}: PolymorphicComponentPropWithRef<C, TextProps>) {
const Component = as || 'span';
return (
<Component className={`text-${color}`} {...props}>
{children}
</Component>
);
}
// Can be used as any element
<Text>Default span</Text>
<Text as="h1">Heading</Text>
<Text as="a" href="/link">Link</Text> // href is type-safe!
// Component with display name
const MyComponent = ({ name }: { name: string }) => {
return <div>{name}</div>;
};
MyComponent.displayName = 'MyComponent'; // For React DevTools
// Component with static methods
interface TabsComponent {
({ children }: { children: React.ReactNode }): JSX.Element;
Panel: ({ children }: { children: React.ReactNode }) => JSX.Element;
}
const Tabs: TabsComponent = ({ children }) => {
return <div className="tabs">{children}</div>;
};
Tabs.Panel = ({ children }) => {
return <div className="tab-panel">{children}</div>;
};
// Usage: <Tabs><Tabs.Panel>Content</Tabs.Panel></Tabs>
TypeScript Best Practices: Prefer function declarations over React.FC, explicitly define
children prop when needed, use generics for reusable components, leverage type inference when possible, use
discriminated unions for variant props, add explicit return types for public APIs, use const assertions for
tuple returns, validate external data with runtime checks.