Back

TSX and the Rise of Typed Frontend Components

TSX and the Rise of Typed Frontend Components

You’re building a React component. You pass a string where a number belongs. TypeScript catches it before the browser does. This simple interaction—types preventing runtime errors—explains why TSX has become the default format for modern frontend development.

This article covers what typed frontend components actually mean in practice: props typing, event handling, children patterns, and how TypeScript inference eliminates boilerplate. We’ll also address how React 19’s server and client component split affects your typing strategy.

Key Takeaways

  • TSX typed components catch type mismatches at compile time, reducing runtime errors before code reaches the browser.
  • TypeScript inference handles most event typing automatically—explicit annotations are only needed when extracting handlers to separate functions.
  • Discriminated unions enable type-safe components with mutually exclusive behaviors, where TypeScript narrows types based on a discriminant property.
  • React 19’s server/client boundary introduces serialization constraints: props passed from server to client components must be serializable (no functions, classes, symbols, or other non-serializable values).

What TSX Typed Components Actually Mean

A typed component isn’t just a component with typed props. It’s a component where TypeScript understands the entire contract: what goes in, what comes out, and what happens in between.

With the modern JSX transform (react-jsx), you don’t import React in every file. You write TSX, and the toolchain handles the rest. Vite scaffolds this correctly out of the box:

interface ButtonProps {
  label: string
  disabled?: boolean
}

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

Notice there’s no React.FC here. That pattern is optional—and often unnecessary. Typing props directly on the function parameter is cleaner and avoids the implicit children prop that React.FC adds whether you want it or not.

Props Typing and React TSX Type Safety

Props typing is where most developers start, but React TSX type safety goes deeper. Consider discriminated unions for components with mutually exclusive behaviors:

type LinkButtonProps = 
  | { variant: 'button'; onClick: () => void }
  | { variant: 'link'; href: string }

function LinkButton(props: LinkButtonProps) {
  if (props.variant === 'link') {
    return <a href={props.href}>Navigate</a>
  }
  return <button onClick={props.onClick}>Click</button>
}

TypeScript narrows the type based on variant. You can’t accidentally access href when variant is 'button'. This pattern scales well for complex component APIs.

Event Typing Without the Boilerplate

Event handlers often trip up developers new to TypeScript. The good news: inference handles most cases automatically.

import { useState } from 'react'

function SearchInput() {
  const [query, setQuery] = useState('')

  // TypeScript infers the event type from context
  return (
    <input 
      value={query} 
      onChange={(e) => setQuery(e.currentTarget.value)} 
    />
  )
}

When you extract handlers to separate functions, you’ll need explicit types:

import type { ChangeEvent } from 'react'

function handleChange(event: ChangeEvent<HTMLInputElement>) {
  // event.currentTarget.value is correctly typed as string
}

Children Typing: ReactNode vs ReactElement

For children, React.ReactNode covers the common case—anything renderable:

import type { ReactNode } from 'react'

interface CardProps {
  children: ReactNode
}

Use React.ReactElement when you need strictly JSX elements, excluding strings and numbers. For components that accept children, PropsWithChildren is a cleaner alternative to manually adding the children prop:

import type { PropsWithChildren } from 'react'

interface CardProps {
  title: string
}

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

Server and Client Components Typing in React 19

React 19 TypeScript projects, especially those using Next.js App Router, split components into server and client boundaries. This affects typing.

Server components can be async and fetch data directly:

// Server Component - no directive needed (default in App Router)
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId)
  return <div>{user.name}</div>
}

Client components require the 'use client' directive and can use hooks:

'use client'

import { useState } from 'react'

interface CounterProps {
  initialCount: number
}

function Counter({ initialCount }: CounterProps) {
  const [count, setCount] = useState(initialCount)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

The typing boundary matters: props passed from server to client components must be serializable. Functions, classes, symbols, and non-serializable values (such as class instances or Dates) won’t cross that boundary.

Form Actions and Modern Typed Workflows

React 19 introduces server actions (commonly used with forms) as a typed pattern for handling submissions, typically defined in server files and invoked from client components:

async function submitForm(formData: FormData) {
  'use server'
  const email = formData.get('email')
  if (typeof email !== 'string') {
    throw new Error('Email is required')
  }
  // Process submission
}

This integrates with typed frontend components by keeping form handling type-aware from client to server.

Conclusion

TSX typed components reduce bugs by catching mismatches at compile time. TypeScript inference minimizes boilerplate—you rarely need explicit type annotations for hooks or inline handlers. The server/client split in React 19 adds a new consideration: serialization boundaries.

Skip React.FC unless you have a specific reason. Type props directly. Let inference work. Your editor will thank you with accurate autocomplete and immediate error feedback.

FAQs

Type props directly on function parameters. React.FC adds an implicit children prop whether you want it or not, and typing props directly is cleaner. The React team does not recommend React.FC as the default pattern. Use it only when you specifically need its behavior, such as when working with legacy codebases that expect it.

For inline handlers, TypeScript infers the event type automatically from context. When you extract handlers to separate functions, use explicit types like React.ChangeEvent for input changes or React.MouseEvent for clicks. The generic parameter specifies the element type, such as HTMLInputElement or HTMLButtonElement.

ReactNode accepts anything renderable including strings, numbers, null, undefined, and JSX elements. ReactElement accepts only JSX elements, excluding primitives. Use ReactNode for most cases. Use ReactElement when your component specifically requires JSX children and should reject plain text or numbers.

No. Props passed from server to client components must be serializable. Functions, classes, symbols, and other non-serializable values cannot cross the server-client boundary. Define event handlers and callbacks within client components, or use server actions for form submissions that need server-side processing.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay