TypeScript With React
Type React components, prop interfaces, event handlers, hooks, and DOM refs with full static safety.
TL;DR
- Define component contracts using strongly typed
interfaceprops declarations. - Type synthetic DOM event handlers using
React.ChangeEventparameters. - Manage nullable state in hooks using explicit
useStategenerics.
Component Props and Typing
Interface props definitionDefine typed props with optional defaults.
interface ButtonProps {
label: string;
onClick: () => void;
}
function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}Native HTML prop extensionInherit standard native button attributes.
import type { ComponentPropsWithoutRef } from "react";
interface BaseBtnProps
extends ComponentPropsWithoutRef<"button"> {
variant: "primary" | "secondary";
}Typed children propAccept any renderable React node child.
import type { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<SectionCard title={title}>
{children}
</SectionCard>
);
}Event Handlers
Input change handlerRead a typed text input value.
import type { ChangeEvent } from "react";
const onInput = (e: ChangeEvent<HTMLInputElement>) =>
e.currentTarget.value;Form submit handlerPrevent reload with a typed form event.
import type { FormEvent } from "react";
const onSubmit = (e: FormEvent<HTMLFormElement>) =>
e.preventDefault();Mouse click handlerRead a button click position.
import type { MouseEvent } from "react";
const onClick = (e: MouseEvent<HTMLButtonElement>) =>
console.log(e.clientX, e.clientY);Keyboard event handlerCheck for Enter in a typed input event.
import type { KeyboardEvent } from "react";
const onKey = (e: KeyboardEvent<HTMLInputElement>) =>
e.key === "Enter" && console.log("Enter");Typing React Hooks
DOM element ref hookType DOM reference with HTML element.
import { useRef } from "react";
const inputRef = useRef<HTMLInputElement>(null);
function focus() {
inputRef.current?.focus();
}Typed useReducer hookUse discriminated union actions in reducers.
type Action =
| { type: "increment" }
| { type: "set"; value: number };
function reducer(
state: number,
action: Action
): number {
if (action.type === "set") return action.value;
return state + 1;
}Custom hook tuple returnFreeze return array using as const.
import { useState } from "react";
function useToggle(initial = false) {
const [val, setVal] = useState(initial);
const toggle = () => setVal(v => !v);
return [val, toggle] as const;
}Context and Forward Ref
Typed context creationCreate context with undefined fallback.
import { createContext, useContext } from "react";
interface ThemeCtx { dark: boolean; }
const Ctx = createContext<ThemeCtx | undefined>(
undefined
);
function useTheme() {
const c = useContext(Ctx);
if (!c) throw new Error("Missing ThemeProvider");
return c;
}Typed forward ref componentExpose underlying input DOM node to callers.
import { forwardRef } from "react";
type Props = { label: string };
const CustomInput = forwardRef<
HTMLInputElement,
Props
>(({ label }, ref) => (
<label>
{label}
<input ref={ref} />
</label>
));RefObject vs MutableRefObjectCompare read-only DOM and mutable value refs.
import { useRef } from "react";
const divRef = useRef<HTMLDivElement>(null); // read-only
const countRef = useRef(0); // writable
countRef.current++;TSConfig Configuration for React
Standard JSX transformUse react-jsx without manual React imports.
{
"compilerOptions": {
"jsx": "react-jsx"
}
}DOM library typesInclude browser DOM types in lib array.
{
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"]
}
}Component path aliasesConfigure clean alias paths for components.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"]
}
}
}Tips
- Extend
React.ComponentPropsWithoutRefwhen wrapping native HTML tags to forward standard attributes likeclassNameandonClickautomatically. - Use
React.ReactNodefor children props to support JSX elements, strings, numbers, portals, or conditional null rendering cleanly.
Warnings
- Avoid legacy
React.FCtyping in component declarations, as it enforces unnecessary wrapper abstractions and historical implicit children typing problems. - Never assign
anyto event handler parameters; use specialized types likeReact.MouseEventto preserve target property autocompletion.
In Practice
A complete typed React component featuring controlled input states, event handlers, and structured submit validation.
- Define form state interfaces and component prop contracts.
- Type the input change event handler using ChangeEvent.
- Type the form submission event handler using FormEvent.
- Render typed JSX elements with accessible attributes.
import { useState, type FormEvent } from "react";
interface FormProps {
onSubmit: (email: string) => void;
}
export function SimpleForm({ onSubmit }: FormProps) {
const [val, set] = useState("");
const submit = (e: FormEvent) => {
e.preventDefault();
onSubmit(val);
};
return (
<form onSubmit={submit}>
<input onChange={e => set(e.target.value)} />
<button type="submit">Send</button>
</form>
);
}FAQ
Use React.ChangeEvent as the handler parameter type: const handleChange = (e: React.ChangeEvent. For select dropdowns and textareas, substitute HTMLSelectElement or HTMLTextAreaElement.
Plain functions typed with explicit parameters and return types (such as JSX.Element) provide clearer compiler diagnostics, support generics seamlessly, and avoid legacy quirks where React.FC implicitly injected optional children into props.
Pass a union generic parameter to the hook: const [user, setUser] = useState. TypeScript will ensure that accessing properties on user requires defensive null checks or optional chaining.
JSX.Element represents a valid JSX expression (the return value of React.createElement). React.ReactNode is a broader union that includes JSX.Element, string, number, boolean, null, undefined, and arrays of nodes, making it the ideal type for children.
Use React.forwardRef. Note that the ref type comes first in the generic parameter list, followed by the props type, matching how React passes the forwarded DOM node to the underlying element.