Skip to content
CodeMorph
Learn

Typing React components

Props, children, events, state, refs and context in TypeScript, with the patterns that keep React types accurate and readable.

Intermediate3 min readUpdated 11 October 2026

React and TypeScript fit well together, as long as you type components the way they're actually used. This guide covers the patterns you'll reach for most when converting .jsx files to .tsx.

Props

Describe props with a type or interface, and annotate the parameter:

Button.tsx
type ButtonProps = {
  label: string;
  variant?: "primary" | "secondary";
  disabled?: boolean;
  onClick: () => void;
};
 
export function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
  return (
    <button className={variant} disabled={disabled} onClick={onClick}>
      {label}
    </button>
  );
}

Prefer string literal unions like "primary" | "secondary" over plain string for props with a known set of values. Callers get autocomplete and typos become errors.

Children

Use React.ReactNode for anything renderable:

type CardProps = {
  title: string;
  children: React.ReactNode;
};

If a component should accept exactly one element, use React.ReactElement instead.

Extending native elements

A component that wraps an HTML element should accept that element's props:

type InputProps = React.ComponentProps<"input"> & {
  label: string;
};
 
export function Input({ label, ...rest }: InputProps) {
  return (
    <label>
      {label}
      <input {...rest} />
    </label>
  );
}

Now type, placeholder, onChange and every other input attribute are typed for free.

Events

Type handlers with React's event types:

function SearchBox() {
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };
  return (
    <form onSubmit={onSubmit}>
      <input onChange={onChange} />
    </form>
  );
}

When you pass a handler inline, TypeScript infers the event type and you don't need to annotate it.

State

useState infers from the initial value. Add a type argument when the initial value doesn't tell the whole story:

const [count, setCount] = useState(0); // number, inferred
const [user, setUser] = useState<User | null>(null); // starts empty
const [status, setStatus] = useState<"idle" | "loading" | "done">("idle");

Refs

const inputRef = useRef<HTMLInputElement>(null);
 
useEffect(() => {
  inputRef.current?.focus();
}, []);

In React 19, function components receive ref as a regular prop, so you can type it in your props:

type FieldProps = React.ComponentProps<"input"> & { ref?: React.Ref<HTMLInputElement> };

On earlier versions, use forwardRef<HTMLInputElement, FieldProps>.

Context

Give context a type and handle the "no provider" case explicitly:

type Theme = "light" | "dark";
const ThemeContext = createContext<Theme | undefined>(undefined);
 
export function useTheme(): Theme {
  const theme = useContext(ThemeContext);
  if (!theme) throw new Error("useTheme must be used inside ThemeProvider");
  return theme;
}

Custom hooks

Let return types be inferred for simple hooks, and annotate them for hooks other people depend on:

export function useToggle(initial = false): [boolean, () => void] {
  const [on, setOn] = useState(initial);
  return [on, () => setOn((v) => !v)];
}

Converting an existing app

Migrate leaf components first: buttons, inputs, icons and small presentational pieces. Then the components that render them, then pages, then your app shell. Each component's props are then inferred from how it's really rendered by components that are already typed. That's the order CodeMorph plans from your imports.

Common pitfalls

  • React.FC: optional. Annotating props directly, as above, is simpler and avoids surprises with children.
  • any in event handlers: use the specific event type, or let TypeScript infer it inline.
  • Optional props used as required: if a prop can be missing, give it a default or handle undefined.
  • Spreading unknown props onto DOM elements: extend React.ComponentProps<"element"> so only valid attributes get through.
Next guidetsconfig.json explained for migrations

Let CodeMorph do the heavy lifting.

Download the beta, open a project and see your migration plan in under a minute.