⚛ React TypeScript
21 Jan, 2026
On this page
React TypeScript cheatsheet is my bible for all React/TypeScript things.
Typing components Jump to heading
The preferred method is to type props directly rather than using FC.
interface ButtonProps {
label: string
onClick: () => void
disabled?: boolean
}
const Button = ({ label, onClick, disabled }: ButtonProps) => {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
With children Jump to heading
Explicitly type children when you need them:
import type { ReactNode } from 'react'
interface CardProps {
title: string
children: ReactNode
}
const Card = ({ title, children }: CardProps) => (
<div>
<h2>{title}</h2>
{children}
</div>
)
With PropsWithChildren Jump to heading
If you want a shorthand for adding children:
import type { PropsWithChildren } from 'react'
interface CardProps {
title: string
}
const Card = ({ title, children }: PropsWithChildren<CardProps>) => (
<div>
<h2>{title}</h2>
{children}
</div>
)
Why not FC?
Jump to heading
FC (FunctionComponent) is no longer recommended because:
- It used to implicitly include
children(fixed in React 18, but the habit stuck) - It doesn’t play well with generics
- Direct prop typing is more explicit and flexible
// ❌ Less preferred
const Button: FC<ButtonProps> = ({ label }) => <button>{label}</button>
// ✅ Preferred
const Button = ({ label }: ButtonProps) => <button>{label}</button>
Note:
VFC(VoidFunctionComponent) was deprecated in React 18 and removed in React 19.
Basic prop types Jump to heading
type AppProps = {
message: string
count: number
disabled: boolean
names: string[]
status: 'waiting' | 'success' | 'error'
/** Object with specific shape */
user: {
id: string
name: string
}
/** Array of objects */
items: {
id: string
title: string
}[]
/** Record type for dictionaries */
scores: Record<string, number>
/** Function that returns nothing */
onClick: () => void
/** Function with parameters */
onChange: (value: string) => void
/** Optional prop */
optional?: string
}
Event handling Jump to heading
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log(event.currentTarget)
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
// ...
}
}
Common prop patterns Jump to heading
import type { ReactNode, CSSProperties, ComponentPropsWithoutRef } from 'react'
interface Props {
/** Anything React can render */
children: ReactNode
/** Style object */
style?: CSSProperties
/** Class name */
className?: string
}
Extending HTML elements Jump to heading
import type { ComponentPropsWithoutRef } from 'react'
interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
variant: 'primary' | 'secondary'
}
const Button = ({ variant, children, ...rest }: ButtonProps) => (
<button className={`btn-${variant}`} {...rest}>
{children}
</button>
)
With ref forwarding Jump to heading
import { forwardRef, type ComponentPropsWithRef } from 'react'
interface InputProps extends ComponentPropsWithRef<'input'> {
label: string
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, ...rest }, ref) => (
<label>
{label}
<input ref={ref} {...rest} />
</label>
)
)
Hooks Jump to heading
useState Jump to heading
// Type is inferred
const [count, setCount] = useState(0)
// Explicit type for complex state
const [user, setUser] = useState<User | null>(null)
// Union types
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle')
useRef Jump to heading
// DOM element ref
const inputRef = useRef<HTMLInputElement>(null)
// Mutable ref (no null)
const intervalRef = useRef<number | undefined>(undefined)
useReducer Jump to heading
type State = { count: number }
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset'; payload: number }
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
useContext Jump to heading
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
Generic components Jump to heading
interface ListProps<T> {
items: T[]
renderItem: (item: T) => ReactNode
}
const List = <T,>({ items, renderItem }: ListProps<T>) => (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
)
// Usage
<List items={users} renderItem={(user) => <span>{user.name}</span>} />
Discriminated unions for props Jump to heading
type ButtonProps =
| { variant: 'link'; href: string }
| { variant: 'button'; onClick: () => void }
const Button = (props: ButtonProps) => {
if (props.variant === 'link') {
return <a href={props.href}>Click me</a>
}
return <button onClick={props.onClick}>Click me</button>
}
← Back home