TypeScript with React
IntermediateTyping 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.
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.
// 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 boxHooks and Context
Typing a custom hook's return, and the context pattern that removes the undefined check everywhere.
// 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 AriaWhy avoid React.FC when typing a component?
Why does a custom hook returning [value, setter] need `as const`?
How do you type a context so consumers never have to check for undefined?
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.