Typing Events & Refs
Event handlers and refs are where React meets the DOM, and they are also where loose typing causes the most pain. Without precise types you lose autocomplete on event.target, you cannot tell which element a ref points at, and null checks get forgotten. This page shows how to type handlers with React’s synthetic event types, how to give useRef a concrete element type, and how to forward refs through your own components.
Typing event handlers
React wraps every native DOM event in a synthetic event — a cross-browser wrapper exposed through generic types like React.ChangeEvent<T>, React.MouseEvent<T>, and React.FormEvent<T>. The generic parameter T is the element the handler is attached to, which is what makes event.target and event.currentTarget strongly typed.
import { ChangeEvent, FormEvent, MouseEvent } from "react";
function SignupForm() {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
// event.target is typed as HTMLInputElement, so .value is a string
console.log(event.target.value);
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log("submitted");
};
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
console.log(event.currentTarget.disabled);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button type="submit" onClick={handleClick}>
Send
</button>
</form>
);
}
You rarely have to write these types when the handler is defined inline, because React infers them from the JSX attribute. Annotations matter most when the handler is a standalone function, as above.
Prefer
event.currentTargetoverevent.targetinside handlers.currentTargetis always the element the listener is bound to (and is correctly typed asT), whereastargetis whatever was actually clicked and may be a child element.
Common event types
| React type | Fired by | Typical element generic |
|---|---|---|
ChangeEvent<T> | onChange on inputs, selects, textareas | HTMLInputElement, HTMLSelectElement |
FormEvent<T> | onSubmit, onReset | HTMLFormElement |
MouseEvent<T> | onClick, onMouseEnter | HTMLButtonElement, HTMLDivElement |
KeyboardEvent<T> | onKeyDown, onKeyUp | HTMLInputElement |
FocusEvent<T> | onFocus, onBlur | HTMLInputElement |
To type an entire handler in one shot — including its return type — use the React.ChangeEventHandler<T> family instead of typing the parameter:
import { ChangeEventHandler } from "react";
const onSearch: ChangeEventHandler<HTMLInputElement> = (event) => {
console.log(event.target.value);
};
Typing element refs
useRef is generic, and the type you pass determines what ref.current holds. For a DOM ref, pass the element interface and initialize with null, because the element does not exist until React attaches it after the first render.
import { useRef, useEffect } from "react";
function SearchBox() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// inputRef.current is HTMLInputElement | null — guard before use
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Search..." />;
}
The crucial detail is the null argument. useRef<HTMLInputElement>(null) produces a read-only RefObject whose current is HTMLInputElement | null — exactly what a DOM ref needs. If you instead write useRef<number>(0) for a mutable value, current becomes a writable number you can reassign freely.
Output:
// After mount, the input gains focus automatically.
Always optional-chain (
ref.current?.method()) or null-check DOM refs. TypeScript includesnullin the type precisely because the element may not be mounted yet.
Forwarding refs
When a parent needs a ref to a DOM node inside a child component, the child must forward it. In React 19 a ref can be accepted as a normal prop, but the widely supported pattern uses forwardRef, which takes two generics: the element type and the props type.
import { forwardRef, ComponentProps, useRef } from "react";
type TextInputProps = ComponentProps<"input"> & { label: string };
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, ...rest }, ref) => (
<label>
{label}
<input ref={ref} {...rest} />
</label>
)
);
function Form() {
const ref = useRef<HTMLInputElement>(null);
return (
<form>
<TextInput ref={ref} label="Email" name="email" />
<button type="button" onClick={() => ref.current?.focus()}>
Focus
</button>
</form>
);
}
The first generic (HTMLInputElement) is what the parent’s ref will point to; the second (TextInputProps) describes the component’s props. With React 19 you can drop forwardRef entirely and declare ref: Ref<HTMLInputElement> as a regular prop, but forwardRef remains the safe choice for libraries and older runtimes.
Best Practices
- Type standalone handlers with
React.ChangeEvent<T>/MouseEvent<T>/FormEvent<T>; let inline handlers infer. - Set the element generic (
<HTMLInputElement>) soevent.target/currentTargetare fully typed. - Prefer
event.currentTargetoverevent.targetfor the bound element’s type and identity. - Initialize DOM refs with
useRef<T>(null)and always optional-chain before touchingcurrent. - Use the
EventHandleraliases when you want to type a handler variable in one annotation. - Pass both generics to
forwardRef<ElementType, PropsType>, or acceptrefas a prop in React 19. - Extend
ComponentProps<"input">on forwarding wrappers so native attributes pass straight through.