webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
TypeScript · Lesson 6 of 6

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.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.