| name | react-typescript |
| description | Complete React TypeScript system. PROACTIVELY activate for: (1) Component props typing, (2) Event handler types, (3) Hooks with TypeScript, (4) Generic components, (5) forwardRef typing, (6) Context with type safety, (7) Utility types (Partial, Pick, Omit), (8) Discriminated unions for state. Provides: Props interfaces, event types, generic patterns, type-safe context, polymorphic components. Ensures type-safe React with proper TypeScript patterns. |
Quick Reference
| Type | Usage | Example |
|---|
| Props interface | Component props | interface ButtonProps { variant: 'primary' } |
ReactNode | Children | children: ReactNode |
ChangeEvent | Input change | (e: ChangeEvent<HTMLInputElement>) |
FormEvent | Form submit | (e: FormEvent<HTMLFormElement>) |
MouseEvent | Click | (e: MouseEvent<HTMLButtonElement>) |
| Pattern | Example |
|---|
| Extend HTML props | extends ButtonHTMLAttributes<HTMLButtonElement> |
| Generic component | function List<T>({ items }: { items: T[] }) |
| forwardRef | forwardRef<HTMLInputElement, Props> |
| Discriminated union | { status: 'success'; data: T } | { status: 'error'; error: Error } |
| Utility Type | Purpose |
|---|
Partial<T> | All props optional |
Pick<T, K> | Select specific props |
Omit<T, K> | Exclude specific props |
ComponentProps<'button'> | Get element props |
When to Use This Skill
Use for React TypeScript integration:
- Typing component props and children
- Handling events with proper types
- Building generic reusable components
- Creating type-safe context and hooks
- Using utility types for prop manipulation
- Implementing polymorphic components
For React basics: see react-fundamentals-19
React with TypeScript
Component Props
Basic Props Types
function Greeting({ name, age }: { name: string; age: number }) {
return <p>Hello {name}, you are {age} years old</p>;
}
interface UserCardProps {
name: string;
email: string;
avatar?: string;
role: 'admin' | 'user' | 'guest';
}
function UserCard({ name, email, avatar, role }: UserCardProps) {
return (
<div className="user-card">
{avatar && <img src={avatar} alt={name} />}
<h3>{name}</h3>
<p>{email}</p>
<span className={`badge-${role}`}>{role}</>
);
}
= | | ;
= | | ;
= {
?: ;
?: ;
: .;
?: ;
};
() {
(
);
}
Children Props
import { ReactNode, PropsWithChildren } from 'react';
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
type ContainerProps = PropsWithChildren<{
className?: string;
}>;
function Container({ className, children }: ContainerProps) {
return <div className={className}>{children}</div>;
}
interface DataFetcherProps<T> {
url: string;
children: (data: T, loading: ) => ;
}
<T>({ url, children }: <T>) {
[data, setData] = useState<T | >();
[loading, setLoading] = ();
;
}
Extending HTML Element Props
import { ButtonHTMLAttributes, InputHTMLAttributes, forwardRef } from 'react';
interface CustomButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
isLoading?: boolean;
}
const CustomButton = forwardRef<HTMLButtonElement, CustomButtonProps>(
({ variant = 'primary', isLoading, children, className, disabled, ...props }, ref) => {
return (
<button
ref={ref}
className={`btn btn-${variant} ${className || ''}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? 'Loading...' : children}
</button>
);
}
);
CustomButton.displayName = 'CustomButton';
interface TextInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
: ;
?: ;
?: | | ;
}
= forwardRef<, >(
{
(
);
}
);
. = ;
Polymorphic Components
import { ElementType, ComponentPropsWithoutRef, ReactNode } from 'react';
type PolymorphicProps<E extends ElementType> = {
as?: E;
children: ReactNode;
} & Omit<ComponentPropsWithoutRef<E>, 'as' | 'children'>;
function Box<E extends ElementType = 'div'>({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || 'div';
return <Component {...props}>{children}</Component>;
}
function App() {
return (
<>
<Box>Default div</Box>
<Box as="section" className="section">Section element</Box>
<Box = =>Link element
console.log('clicked')}>Button
);
}
Event Handlers
Common Event Types
import {
ChangeEvent,
FormEvent,
MouseEvent,
KeyboardEvent,
FocusEvent,
DragEvent,
} from 'react';
function EventExamples() {
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log(Object.fromEntries(formData));
};
= () => {
.(e., e.);
};
= () => {
(e. === ) {
.();
}
};
= () => {
.(, e..);
};
= () => {
e..(, );
};
(
);
}
Event Handler Props
interface FormFieldProps {
onChange: (value: string) => void;
onBlur?: () => void;
}
function FormField({ onChange, onBlur }: FormFieldProps) {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};
return <input onChange={handleChange} onBlur={onBlur} />;
}
interface ListItemProps<T> {
item: T;
onSelect: (item: T) => void;
onDelete?: (item: T) => void;
}
function ListItem<T extends { id: string; name: string }>({
item,
onSelect,
onDelete,
}: ListItemProps<T>) {
return (
);
}
Hooks with TypeScript
useState
import { useState } from 'react';
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
interface FormState {
name: string;
email: string;
errors: Record<string, string>;
}
const [form, setForm] = useState<FormState>({
name: '',
email: '',
errors: {},
});
setForm(prev => ({ ...prev, name: 'John' }));
useReducer
import { useReducer, Reducer } from 'react';
interface CounterState {
count: number;
step: number;
}
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; payload: number };
const counterReducer: Reducer<CounterState, CounterAction> = (state, action) => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'reset':
return { ...state, count: 0 };
case 'setStep':
return { ...state, : action. };
:
state;
}
};
() {
[state, dispatch] = (counterReducer, { : , : });
(
);
}
useRef
import { useRef, useEffect } from 'react';
function RefExamples() {
const inputRef = useRef<HTMLInputElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const countRef = useRef<number>(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
inputRef.current?.focus();
const ctx = canvasRef.current?.getContext('2d');
if (ctx) {
ctx.fillRect(0, 0, 100, 100);
}
timerRef.current = setInterval(() => {
countRef.current += 1;
}, 1000);
return () => {
if (timerRef.current) {
(timerRef.);
}
};
}, []);
(
);
}
useContext
import { createContext, useContext, useState, ReactNode } from 'react';
interface Theme {
primary: string;
secondary: string;
mode: 'light' | 'dark';
}
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleMode: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>({
primary: '#007bff',
secondary: '#6c757d',
mode: 'light',
});
const toggleMode = () => {
setTheme((prev) => ({
...prev,
mode: prev.mode === ? : ,
}));
};
(
);
}
() {
context = ();
(!context) {
();
}
context;
}
() {
{ theme, toggleMode } = ();
(
);
}
Custom Hooks
import { useState, useEffect, useCallback } from 'react';
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = response.();
(result);
} (err) {
(err ? err : ());
} {
();
}
}, [url]);
( {
();
}, [fetchData]);
{ data, loading, error, : fetchData };
}
{
: ;
: ;
: ;
}
() {
{ : user, loading, error } = useFetch<>();
(loading) ;
(error) ;
(!user) ;
(
);
}
Generic Components
Generic List
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({
items,
renderItem,
keyExtractor,
emptyMessage = 'No items',
}: ListProps<T>) {
if (items.length === 0) {
return <p>{emptyMessage}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
interface Product {
id: string;
name: string;
price: number;
}
function ProductList({ products }: { products: Product[] }) {
(
);
}
Generic Select
interface SelectOption<T> {
value: T;
label: string;
}
interface SelectProps<T> {
options: SelectOption<T>[];
value: T | null;
onChange: (value: T) => void;
placeholder?: string;
getOptionValue?: (option: SelectOption<T>) => string;
}
function Select<T>({
options,
value,
onChange,
placeholder = 'Select...',
getOptionValue = (opt) => String(opt.value),
}: SelectProps<T>) {
const selectedOption = options.find((opt) => opt.value === value);
return (
<select
value={selectedOption ? getOptionValue(selectedOption) : ''}
onChange={(e) => {
const option = options.find(
(opt) => getOptionValue(opt) === e.target.value
);
if (option) {
onChange(option.value);
}
}}
>
<option value= >
{placeholder}
{options.map((option) => (
{option.label}
))}
);
}
= | | ;
() {
[status, setStatus] = useState< | >();
: <>[] = [
{ : , : },
{ : , : },
{ : , : },
];
;
}
Generic Table
interface Column<T> {
key: keyof T | string;
header: string;
render?: (item: T) => ReactNode;
width?: string | number;
}
interface TableProps<T> {
data: T[];
columns: Column<T>[];
keyExtractor: (item: T) => string | number;
onRowClick?: (item: T) => void;
}
function Table<T extends Record<string, unknown>>({
data,
columns,
keyExtractor,
onRowClick,
}: TableProps<T>) {
const getCellValue = (item: T, column: Column<T>): ReactNode => {
if (column.render) {
return column.render(item);
}
const value = item[column.key as keyof T];
return value as ReactNode;
};
(
);
}
{
: ;
: ;
: ;
: | ;
: ;
}
() {
: <>[] = [
{ : , : },
{ : , : },
{
: ,
: ,
: (
),
},
{
: ,
: ,
: user..(),
},
];
(
);
}
Type Utilities
Common Utility Types
interface User {
id: string;
name: string;
email: string;
}
type PartialUser = Partial<User>;
interface Config {
host?: string;
port?: number;
}
type RequiredConfig = Required<Config>;
type UserPreview = Pick<User, 'id' | 'name'>;
type CreateUserInput = Omit<User, 'id'>;
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
= | | | ;
= <, | >;
= <, >;
Component Props Utilities
import { ComponentProps, ComponentPropsWithRef, ComponentPropsWithoutRef } from 'react';
type ButtonProps = ComponentProps<'button'>;
type DivProps = ComponentProps<'div'>;
function MyButton(props: { variant: 'primary' | 'secondary' }) {
return <button {...props} />;
}
type MyButtonProps = ComponentProps<typeof MyButton>;
type InputPropsWithRef = ComponentPropsWithRef<'input'>;
type InputPropsNoRef = ComponentPropsWithoutRef<'input'>;
Discriminated Unions
type ApiResponse<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function useApiData<T>(url: string): ApiResponse<T> {
return { status: 'idle' };
}
function DataDisplay() {
const response = useApiData<User[]>('/api/users');
switch (response.status) {
case 'idle':
return <p>Ready to fetch</p>;
case 'loading':
return <p>Loading...</p>;
case 'success':
return <UserList users={response.data} />;
:
;
}
}
Inference and Conditional Types
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
function fetchUser(id: string) {
return { id, name: 'John', email: 'john@example.com' };
}
type FetchUserReturn = ReturnTypeOf<typeof fetchUser>;
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function getUsers() {
return [{ id: '1', name: 'John' }];
}
type UsersData = Awaited<ReturnType<typeof getUsers>>;
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
Type-Safe Context
import { createContext, useContext, ReactNode } from 'react';
function createSafeContext<T>(displayName: string) {
const Context = createContext<T | undefined>(undefined);
Context.displayName = displayName;
function useContextSafe() {
const context = useContext(Context);
if (context === undefined) {
throw new Error(`use${displayName} must be used within ${displayName}Provider`);
}
return context;
}
return [Context.Provider, useContextSafe] as const;
}
interface AuthContextValue {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
[, useAuth] = createSafeContext<>();
() {
[user, setUser] = useState< | >();
= () => {
};
= () => {
();
};
(
);
}
() {
{ user, logout } = ();
(!user) ;
(
);
}
Best Practices
| Practice | Example |
|---|
| Use interface for component props | interface ButtonProps { ... } |
| Prefer type inference when obvious | useState(0) vs useState<number>(0) |
| Use generics for reusable components | List<T>, Select<T> |
| Discriminated unions for state | { status: 'success'; data: T } |
| forwardRef with proper types | forwardRef<HTMLButtonElement, Props> |
Avoid any, use unknown if needed | catch (err: unknown) |
Use as const for literal types | ['a', 'b'] as const |