Notifications

No notifications

/Phase 4

TypeScript with React

React + TypeScript is the dominant front-end stack — and the one this very project uses. You'll type props, state, refs, events, children, and generic components. Get this right and your IDE basically writes the JSX for you.

On this page

Detailed Theory

Typing component props

Use a type or interface for props. Both work — convention is Props suffix:

type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
};

function Button({ label, onClick, disabled }: ButtonProps) { return <button onClick={onClick} disabled={disabled}>{label}</button>; }

You almost never need React.FC anymore — it adds an implicit children prop and obscures generics.

children and ReactNode

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

function Card({ title, children }: CardProps) { return <section><h2>{title}</h2>{children}</section>; }

ReactNode covers strings, numbers, JSX, arrays, fragments, null. Use ReactElement only when you need a single JSX element.

Hooks

useState

const [count, setCount] = useState(0);          // inferred number
const [user, setUser] = useState<User | null>(null); // explicit when initial doesn't carry the full shape

useReducer

Type the state and the action union — narrowing then works inside the reducer:

type State = { count: number };
type Action =
  | { type: "inc" }
  | { type: "dec" }
  | { type: "set"; value: number };

function reducer(state: State, action: Action): State { switch (action.type) { case "inc": return { count: state.count + 1 }; case "dec": return { count: state.count - 1 }; case "set": return { count: action.value }; } }

const [state, dispatch] = useReducer(reducer, { count: 0 });

useRef

Two flavours:

const inputRef = useRef<HTMLInputElement>(null);   // DOM ref
const idRef = useRef(0);                            // mutable value

DOM refs are null until React attaches them — TS makes you check.

useEffect / useCallback / useMemo

Mostly inferred, but type their generics when the inferred type isn't tight enough:

const onSubmit = useCallback<(e: FormEvent) => void>((e) => { /* … */ }, []);
const total = useMemo<number>(() => items.reduce((s, x) => s + x.price, 0), [items]);

Event handlers

Always grab the event type from React:

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value);
}

function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); }

function handleClick(e: React.MouseEvent<HTMLButtonElement>) { console.log(e.currentTarget.dataset.id); }

Cheat sheet:

EventType
ChangeReact.ChangeEvent
SubmitReact.FormEvent
ClickReact.MouseEvent
KeyReact.KeyboardEvent
FocusReact.FocusEvent

e.target is the actual element that fired the event; e.currentTarget is the element the handler is attached to. currentTarget is usually what you want.

Generic components

Yes — components can be generic:

type ListProps<T> = {
  items: T[];
  render: (item: T) => React.ReactNode;
};

function List<T>({ items, render }: ListProps<T>) { return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>; }

// Usage — T is inferred from items <List items={[{ id: 1, name: "Ada" }, { id: 2, name: "Linus" }]} render={(u) => u.name} // u is { id: number; name: string } />

as const for tuple state

If a custom hook returns a tuple, mark it as const so consumers get a tuple, not (T | F)[]:

function useToggle(init = false) {
  const [on, setOn] = useState(init);
  return [on, () => setOn((v) => !v)] as const;
  // type: readonly [boolean, () => void]
}

Forwarding refs

type InputProps = React.InputHTMLAttributes<HTMLInputElement>;

const Input = React.forwardRef<HTMLInputElement, InputProps>( function Input(props, ref) { return <input ref={ref} {...props} />; } );

Tips that save hours

  • ✅ Prefer type for component props — supports unions, intersections, mapped types.
  • ✅ Don't use React.FC unless you have a reason; it hurts generics & adds implicit children.
  • ✅ Type events on the handler parameter, not on useState.
  • ✅ Lean on React.ComponentProps<"button"> to inherit native props instead of retyping them.
  • tsc --noEmit in CI catches real bugs your tests won't.