Home/Learn/JavaScript & TypeScript/TypeScript with React

TypeScript with React

Intermediate
TypeScript

Typing props, state, events, refs and children. This is where most TypeScript in a frontend job actually lives.

Overview

React with TypeScript is a small set of patterns you use constantly. Props are an interface. State usually infers, except when it starts null or empty. Event handlers have specific types that autocomplete once you know the naming. Refs and children have their own conventions. Getting these right removes almost all of the friction people report with the combination — most of the pain comes from fighting inference rather than working with it.

Props and Children

A plain interface, and no React.FC.

Props interfaces and ComponentPropsWithoutRef
interface ProblemCardProps {
  problem: Problem
  locked?: boolean
  onOpen: (slug: string) => void
  children?: React.ReactNode
}

export function ProblemCard({ problem, locked = false, onOpen, children }: ProblemCardProps) {
  ...
}

// Avoid React.FC — it adds an implicit children prop and complicates generics.
// A plain annotated parameter is clearer.

// Extending native element props — the pattern for design-system components
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
  variant?: 'primary' | 'ghost'
}
export function Button({ variant = 'primary', ...rest }: ButtonProps) {
  return <button className={styles[variant]} {...rest} />
}
// Now className, disabled, onClick, aria-* all typecheck for free.

// ReactNode = anything renderable. ReactElement = specifically an element.

State, Events and Refs

The three places annotations are actually needed.

useState, event types, useRef
// Infers fine
const [count, setCount] = useState(0)

// Needs help — starts null or empty
const [user, setUser] = useState<User | null>(null)
const [items, setItems] = useState<Problem[]>([])

// Discriminated union state, which beats three booleans
const [state, setState] = useState<RequestState>({ status: 'idle' })

// Events — inferred inline, annotated when extracted
<input onChange={(e) => setQuery(e.target.value)} />   // inferred

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... }
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { ... }

// Refs — the element type, initialised to null
const inputRef = useRef<HTMLInputElement>(null)     // for JSX ref
inputRef.current?.focus()

const timer = useRef<number | undefined>(undefined)  // mutable box

Hooks and Context

Typing a custom hook's return, and the context pattern that removes the undefined check everywhere.

as const for tuples, and the context guard
// as const makes the tuple a tuple, not an array union
function useToggle(initial = false) {
  const [on, setOn] = useState(initial)
  const toggle = useCallback(() => setOn(v => !v), [])
  return [on, toggle] as const     // readonly [boolean, () => void]
}
const [open, toggleOpen] = useToggle()    // correctly typed

// Context — the standard pattern
const AuthContext = createContext<AuthValue | undefined>(undefined)

export function useAuth(): AuthValue {
  const ctx = useContext(AuthContext)
  if (!ctx) throw new Error('useAuth must be used inside <AuthProvider>')
  return ctx        // non-optional from here on
}
// The throw both catches the mistake and narrows the type, so no
// consumer needs to handle undefined.

// A generic component
function List<T>({ items, render }: { items: T[]; render: (item: T) => React.ReactNode }) {
  return <ul>{items.map(render)}</ul>
}

Key Points to Remember

  • 1Type props with a plain interface and annotate the parameter — React.FC adds an implicit children prop
  • 2Extend React.ComponentPropsWithoutRef<"button"> to inherit every native prop on a wrapper component
  • 3useState infers from the initial value, so annotate only when it starts null or as an empty array
  • 4useRef<HTMLInputElement>(null) for DOM refs; access through optional chaining
  • 5Return `as const` from a custom hook so the result is a tuple, and throw in the context hook to narrow away undefined

Interview Questions

Sign in to ask Aria
1

Why avoid React.FC when typing a component?

Medium
2

Why does a custom hook returning [value, setter] need `as const`?

Medium
3

How do you type a context so consumers never have to check for undefined?

Medium

Ask Aria about TypeScript with React

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…