Difficulty: Advanced
React's useState hook is a generic function that can infer the state type from its initial value or accept an explicit type parameter. When you write `useState(0)`, TypeScript infers the state type as `number`. When you write `useState<string | null>(null)`, you explicitly tell TypeScript that the state can be either a string or null. Explicit typing is essential when the initial value does not represent all possible future values of the state.
Event handling in React with TypeScript requires understanding the synthetic event system. React wraps native browser events in `SyntheticEvent` objects, and TypeScript provides specific generic event types for each: `React.MouseEvent`, `React.ChangeEvent`, `React.FormEvent`, `React.KeyboardEvent`, and more. Each event type is generic over the HTML element that triggered it, such as `React.ChangeEvent<HTMLInputElement>` or `React.MouseEvent<HTMLButtonElement>`.
Form events are particularly important in React applications. The `onChange` handler for input elements uses `React.ChangeEvent<HTMLInputElement>`, giving you typed access to `event.target.value`. The `onSubmit` handler for form elements uses `React.FormEvent<HTMLFormElement>`, and you typically call `event.preventDefault()` to prevent page reload. When typing event handler functions separately from JSX, use `React.EventHandler<E>` or the specific handler type like `React.ChangeEventHandler<HTMLInputElement>`.
A common pattern is to type state as a union that includes the initial state and all possible future states. For example, data fetching state might be typed as `{ status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string }`. This discriminated union pattern ensures you handle all possible states and cannot accidentally access `data` when the status is 'loading'.
// Simulating React useState behavior
function useState<T>(initial: T): [T, (newVal: T | ((prev: T) => T)) => T] {
let state = initial;
const setter = (newVal: T | ((prev: T) => T)): T => {
if (typeof newVal === 'function') {
state = (newVal as (prev: T) => T)(state);
} else {
state = newVal;
}
return state;
};
return [state, setter];
}
// Type inferred as number
const [count, setCount] = useState(0);
console.log(count);
console.log(setCount(5));
// Explicit type for union state
const [user, setUser] = useState<string | null>(null);
console.log(user);
console.log(setUser('Alice'));
// Complex state type
interface FormState {
name: string;
email: string;
age: number;
}
const [form, setForm] = useState<FormState>({ name: '', email: '', age: 0 });
console.log(JSON.stringify(form));
console.log(JSON.stringify(setForm({ name: 'Bob', email: 'bob@test.com', age: 25 })));
useState infers the type from the initial value. Use explicit generics when the initial value (like null) does not represent all possible states.
// Simulating React event types
interface ChangeEvent<T> {
target: T & { value: string; name: string };
preventDefault(): void;
}
interface MouseEvent<T> {
target: T;
clientX: number;
clientY: number;
preventDefault(): void;
}
// Typed change handler
function handleInputChange(event: ChangeEvent<{ type: string }>): void {
console.log(`Input changed: ${event.target.name} = ${event.target.value}`);
}
// Typed click handler
function handleClick(event: MouseEvent<{ id: string }>): void {
console.log(`Clicked at (${event.clientX}, ${event.clientY})`);
}
// Simulate events
handleInputChange({
target: { value: 'john@test.com', name: 'email', type: 'text' },
preventDefault() {}
});
handleClick({
target: { id: 'submit-btn' },
clientX: 150,
clientY: 200,
preventDefault() {}
});
React event types are generic over the element type. ChangeEvent<HTMLInputElement> gives you typed access to target.value and target.name.
// Using discriminated unions for async state
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return 'Ready to fetch';
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error}`;
}
}
const states: AsyncState<string[]>[] = [
{ status: 'idle' },
{ status: 'loading' },
{ status: 'success', data: ['item1', 'item2'] },
{ status: 'error', error: 'Network timeout' }
];
states.forEach(s => console.log(renderState(s)));
The discriminated union pattern ensures exhaustive handling of all async states. TypeScript narrows the type in each case branch, so 'data' is only accessible when status is 'success'.
useState with types, event handler types, form events, ChangeEvent, FormEvent, MouseEvent, state type inference