TypeScript in React and Next
Props, children, state, events, a typed reducer and PageProps.
Updated
What typing React means
In React, TS checks three main things: the props a component receives, the state and the events. The result: autocomplete for props, errors when you forget a required one, fearless refactoring.
Props
type ButtonProps = {
variant?: 'primary' | 'secondary'
size?: 'sm' | 'md'
children: React.ReactNode
onClick?: () => void
}
export function Button({ variant = 'primary', size = 'md', children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>
}Extending a native element's props
Instead of listing type, disabled, aria-*... by hand:
type Props = React.ComponentProps<'button'> & { variant?: 'primary' | 'ghost' }
export function Button({ variant = 'primary', className, ...props }: Props) {
return <button className={cn(variants[variant], className)} {...props} />
}Exactly how shared/ui/button.tsx in this project is written.
Types for children — differences
| Type | Accepts | When |
|---|---|---|
React.ReactNode |
anything renderable: JSX, a string, a number, null, an array |
the default for children |
React.ReactElement |
only a JSX element | when you really need an element (rare) |
string |
only text | simple labels |
State
const [count, setCount] = useState(0) // inference: number
const [user, setUser] = useState<User | null>(null) // an explicit generic when the initial value is null
const [status, setStatus] = useState<'idle' | 'loading'>('idle')Events
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value)
}
function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const data = new FormData(e.currentTarget)
}A trick: write the handler inline (onChange={e => ...}) — TS infers the type; hover over e and copy it.
A reducer with a discriminated union
type Action =
| { type: 'add'; text: string }
| { type: 'toggle'; id: number }
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case 'add': return [...state, { id: Date.now(), text: action.text, done: false }]
case 'toggle': return state.map(t => (t.id === action.id ? { ...t, done: !t.done } : t))
}
}dispatch({ type: 'add' }) without text → an error. See Union types.
Next.js specifics
export default async function Page({ params }: PageProps<'/blog/[slug]'>) {
const { slug } = await params // typed automatically from the folder structure
}
export default function Layout({ children }: LayoutProps<'/'>) { ... }PageProps and LayoutProps are generated by Next from your routes.
Summary
- Props as a
type;ComponentProps<'button'>to extend native elements. children: React.ReactNode;useState<T>when the initial value doesn't say everything.- Events:
ChangeEvent,FormEvent; a reducer with a union of actions.