
Typescript React Patterns
- 477 installs
- 60 repo stars
- Updated May 16, 2026
- asyrafhussin/agent-skills
typescript-react-patterns is an agent skill that applies 33 TypeScript typing rules across 7 categories for developers writing type-safe React components, hooks, events, refs, generics, and context in modern UI codebases
About
typescript-react-patterns is an agent skill from asyrafhussin/agent-skills version 2.0.0 that codifies 33 TypeScript rules across 7 categories for React development. Categories span component typing (props interfaces, forwardRef, polymorphic as props), hook typing (useState, useReducer, useCallback, custom hooks), event handlers, ref patterns including useImperativeHandle, generic list/select/table components, typed Context providers, and utility types like ComponentPropsWithoutRef and discriminated unions. Developers reach for typescript-react-patterns when fixing TypeScript errors in React code, typing new components and hooks, or standardizing folder-level patterns during agent-assisted UI work. Each rule uses prefixed identifiers such as comp-props-interface and hook-use-reducer with detailed explanations in individual rules/ files and a compiled AGENTS.md reference. Priority ranks component and hook typing as CRITICAL, event and ref typing as HIGH, and utility types as LOW impact guidance.
- Component and hook patterns
- Strict TypeScript typing
- Reusable UI architecture
- Agent-friendly React conventions
- Saas and extension UI fit
Typescript React Patterns by the numbers
- 477 all-time installs (skills.sh)
- Ranked #631 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/asyrafhussin/agent-skills --skill typescript-react-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 477 |
|---|---|
| repo stars | ★ 60 |
| Last updated | May 16, 2026 |
| Repository | asyrafhussin/agent-skills ↗ |
How do you type React components and hooks in TypeScript?
Apply proven TypeScript and React patterns for components, hooks, state, typing, and folder structure while building modern UI features in agent-assisted coding sessions.
Who is it for?
React TypeScript developers who want consistent, rule-based typing for components, hooks, and context during agent-assisted feature work.
Skip if: Vue, Svelte, or plain JavaScript React projects where TypeScript strict typing patterns are not required or used.
When should I use this skill?
User writes typed React components or hooks, hits TypeScript errors in React code, or asks for props, event, ref, or generic component typing patterns.
What you get
Type-safe React component props, hook signatures, event handlers, ref types, generic components, and context providers aligned to 33 rules.
- Typed component and hook implementations
- Context provider patterns
- Generic reusable component types
By the numbers
- Version 2.0.0 with 33 rules across 7 categories
- Rule prefixes span comp-, hook-, event-, ref-, generic-, ctx-, and util- namespaces
Files
TypeScript React Patterns
Type-safe React with TypeScript. Contains 33 rules across 7 categories covering component typing, hooks, event handling, refs, generics, context, and utility types.
Metadata
- Version: 2.0.0
- Rule Count: 33 rules across 7 categories
- License: MIT
When to Apply
Reference these guidelines when:
- Typing React component props
- Creating custom hooks with TypeScript
- Handling events with proper types
- Working with refs (DOM, mutable, imperative)
- Building generic, reusable components
- Setting up typed Context providers
- Fixing TypeScript errors in React code
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Component Typing | CRITICAL | comp- |
| 2 | Hook Typing | CRITICAL | hook- |
| 3 | Event Handling | HIGH | event- |
| 4 | Ref Typing | HIGH | ref- |
| 5 | Generic Components | MEDIUM | generic- |
| 6 | Context & State | MEDIUM | ctx- |
| 7 | Utility Types | LOW | util- |
Quick Reference
1. Component Typing (CRITICAL)
comp-props-interface- Use interface for props, type for unionscomp-children-types- Correct children typing (ReactNode, ReactElement)comp-default-props- Default props with destructuring defaultscomp-forward-ref- Typing forwardRef componentscomp-polymorphic- Polymorphic "as" prop typingcomp-fc-vs-function- Function declaration vs React.FCcomp-display-name- Display names for debuggingcomp-rest-props- Spreading rest props with proper types
2. Hook Typing (CRITICAL)
hook-usestate- useState with proper generic typeshook-useref- useRef for DOM elements and mutable valueshook-use-reducer- useReducer with discriminated union actionshook-use-callback- useCallback with typed parametershook-use-memo- useMemo with typed return valueshook-use-context- useContext with null checkinghook-custom-hooks- Custom hook return typeshook-generic-hooks- Generic custom hooks
3. Event Handling (HIGH)
event-handler-types- Event handler type patternsevent-click-handler- Click event typingevent-form- Form event handling (submit, change, select)event-keyboard- Keyboard event types
4. Ref Typing (HIGH)
ref-dom-elements- useRef with specific HTML element typesref-callback- Callback ref pattern for DOM measurementref-imperative-handle- useImperativeHandle typing
5. Generic Components (MEDIUM)
generic-list- Generic list componentsgeneric-select- Generic select/dropdowngeneric-table- Generic table with typed columnsgeneric-constraints- Generic constraints with extends
6. Context & State (MEDIUM)
ctx-create- Creating typed contextctx-provider- Provider pattern with null check hookctx-reducer- Context with useReducer
7. Utility Types (LOW)
util-component-props- ComponentPropsWithoutRef for HTML propsutil-pick-omit- Pick, Omit, Partial for prop derivationutil-discriminated-unions- Discriminated unions for state machines
Essential Patterns
Component Props
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
children: React.ReactNode
onClick?: () => void
}
function Button({ variant, size = 'md', children, onClick }: ButtonProps) {
return (
<button className={`btn-${variant} btn-${size}`} onClick={onClick}>
{children}
</button>
)
}Typed Context with Null Check
interface AuthContextType {
user: User | null
login: (credentials: Credentials) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthContextType | null>(null)
function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}Generic Component
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>
}How to Use
Read individual rule files for detailed explanations:
rules/comp-props-interface.md
rules/hook-usestate.md
rules/event-form.md
rules/ref-dom-elements.md
rules/util-discriminated-unions.mdReferences
Full Compiled Document
For the complete guide with all rules expanded: AGENTS.md
{
"name": "TypeScript React Patterns",
"version": "2.0.0",
"description": "Type-safe React with TypeScript. Contains 33 rules across 7 categories covering component typing, hooks, event handling, refs, generics, context, and utility types.",
"license": "MIT",
"author": {
"name": "Agent Skills Contributors",
"url": "https://github.com/AsyrafHussin/agent-skills"
},
"categories": [
{ "name": "Component Typing", "prefix": "comp", "impact": "CRITICAL", "ruleCount": 8 },
{ "name": "Hook Typing", "prefix": "hook", "impact": "CRITICAL", "ruleCount": 8 },
{ "name": "Event Handling", "prefix": "event", "impact": "HIGH", "ruleCount": 4 },
{ "name": "Ref Typing", "prefix": "ref", "impact": "HIGH", "ruleCount": 3 },
{ "name": "Generic Components", "prefix": "generic", "impact": "MEDIUM", "ruleCount": 4 },
{ "name": "Context & State", "prefix": "ctx", "impact": "MEDIUM", "ruleCount": 3 },
{ "name": "Utility Types", "prefix": "util", "impact": "LOW", "ruleCount": 3 }
],
"references": [
"https://react-typescript-cheatsheet.netlify.app",
"https://react.dev/learn/typescript",
"https://www.typescriptlang.org/docs/handbook/"
],
"keywords": [
"typescript",
"react",
"typed-components",
"hooks",
"generics",
"events",
"refs",
"context"
],
"rulesTotal": 33,
"lastUpdated": "2026-03-14"
}
TypeScript React Patterns v2.0.0
Type-safe React with TypeScript.
Overview
- Component typing (props interface, children, forwardRef, polymorphic, rest props)
- Hook typing (useState, useRef, useReducer, useCallback, useMemo, custom hooks)
- Event handling (form, click, keyboard events)
- Ref typing (DOM elements, callback refs, useImperativeHandle)
- Generic components (list, select, table, constraints)
- Context & state (typed provider, useReducer with discriminated unions)
- Utility types (ComponentProps, Pick/Omit, discriminated unions)
- 33 rules across 7 categories
Categories
1. Component Typing (Critical)
Props interface, children types, default props, forwardRef, polymorphic, FC vs function, display name, rest props.
2. Hook Typing (Critical)
useState, useRef, useReducer, useCallback, useMemo, useContext, custom hooks, generic hooks.
3. Event Handling (High)
Handler types, click events, form events, keyboard events.
4. Ref Typing (High)
DOM element refs, callback refs, useImperativeHandle.
5. Generic Components (Medium)
Generic list, select, table, constraints with extends/keyof.
6. Context & State (Medium)
Typed context creation, provider pattern, context with useReducer.
7. Utility Types (Low)
ComponentPropsWithoutRef, Pick/Omit/Partial, discriminated unions.
Usage
Type this React component properly
Fix TypeScript errors in my React code
Create a generic table component
Set up typed Context with useReducerReferences
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Component Typing (comp)
Impact: CRITICAL Description: Foundational patterns for typing React component props. Interface vs type for props, children typing with ReactNode, default props with destructuring, forwardRef, polymorphic "as" prop, FC vs function declaration, and rest props spreading.
2. Hook Typing (hook)
Impact: CRITICAL Description: Type-safe React hooks. useState with generic types, useRef for DOM elements and mutable values, useReducer with discriminated union actions, useCallback/useMemo with typed parameters, useContext with null checking, and custom hooks with proper return types.
3. Event Handling (event)
Impact: HIGH Description: Typing React event handlers correctly. FormEvent, ChangeEvent, MouseEvent, KeyboardEvent with proper HTML element generics, and event handler prop types.
4. Ref Typing (ref)
Impact: HIGH Description: TypeScript patterns for React refs. useRef with specific HTMLElement types, callback refs for DOM measurement, and useImperativeHandle for exposing component methods to parents.
5. Generic Components (generic)
Impact: MEDIUM Description: Building reusable, type-safe generic components. Generic list, select, and table components with type inference, and generic constraints using extends and keyof.
6. Context & State (ctx)
Impact: MEDIUM Description: Typed React Context patterns. Creating context with null default and custom hook that throws on missing provider, and combining Context with useReducer using discriminated union actions.
7. Utility Types (util)
Impact: LOW Description: TypeScript utility types for React. ComponentPropsWithoutRef for inheriting HTML attributes, Pick/Omit/Partial for deriving prop types, and discriminated unions for modeling state machines with exhaustive checking.
[rule-name]
Why It Matters
[Brief explanation of why this pattern is important. Focus on:
- What problem it solves
- What errors it prevents
- When you need this pattern]
Incorrect
// ❌ [Brief description of what's wrong]
[Bad code example showing the anti-pattern]
// ❌ [Another common mistake]
[Another bad example]Why this is problematic:
- [Specific issue 1]
- [Specific issue 2]
- [What errors this causes]
Correct
// ✅ [Description of the correct approach]
[Good code example]
// ✅ [Alternative correct approach if applicable]
[Another good example]Why this works:
- [Benefit 1]
- [Benefit 2]
- [Type safety gained]
Additional Examples
[Specific Use Case 1]
// Context for when to use this variation
[Code example with explanation][Specific Use Case 2]
// Context for this pattern
[Code example with explanation]Common Patterns
// [Pattern name 1]
[Concise example]
// [Pattern name 2]
[Concise example]Tips
- [Quick tip 1]
- [Common mistake to avoid]
- [Performance consideration if applicable]
- [When NOT to use this pattern]
Related
- See
[related-rule-1.md]for [related concept] - See
[related-rule-2.md]for [related concept]
---
Template Notes (remove these when creating actual rules):
1. Title: Use clear, descriptive titles that match the file name 2. Category: Must match one of the 7 defined categories 3. Priority:
- CRITICAL: Fundamental patterns every project needs
- HIGH: Common patterns most projects use
- MEDIUM: Advanced patterns for specific needs
- LOW: Specialized edge cases
4. Code Examples:
- Use ✅ for correct examples
- Use ❌ for incorrect examples
- Include inline comments explaining key points
- Show both the problem and solution
5. Focus on TypeScript + React:
- Emphasize type safety
- Show how types flow through the code
- Include actual type annotations
- Show IDE autocomplete benefits
6. Be Concise: Each rule should be scannable in 2-3 minutes 7. Show Real Examples: Use realistic code, not foo/bar placeholders
Component Children Types
Impact: CRITICAL (prevents runtime errors from invalid children)
children is one of the most commonly used props. Using the wrong type causes type errors or allows invalid usage. Choose the right type based on what your component accepts.
Incorrect
// ❌ Bad
// Too restrictive - won't accept strings or numbers
interface CardProps {
children: React.ReactElement
}
// This fails:
<Card>Hello</Card> // Error: string is not ReactElement
<Card>{42}</Card> // Error: number is not ReactElement
// No type - any is implied
interface CardProps {
children: any
}
// JSX.Element - React Native incompatible
interface CardProps {
children: JSX.Element
}Problems:
React.ReactElementrejects valid renderable values like strings, numbers, and fragments- Using
anyremoves all type checking and allows invalid children JSX.Elementis not cross-platform compatible
Correct
// ✅ Good
// ReactNode (Most Common) - Accepts anything React can render
interface CardProps {
title: string
children: React.ReactNode
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
)
}
// All valid:
<Card title="Welcome">Hello</Card>
<Card title="Count">{42}</Card>
<Card title="User"><UserProfile /></Card>
<Card title="List">{items.map(i => <Item key={i.id} />)}</Card>
<Card title="Maybe">{showContent && <Content />}</Card>
<Card title="Empty">{null}</Card>
// ReactElement (JSX Only) - When you need to access element props
interface TabsProps {
children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[]
}
interface TabProps {
label: string
children: React.ReactNode
}
function Tabs({ children }: TabsProps) {
const tabs = React.Children.toArray(children) as React.ReactElement<TabProps>[]
return (
<div>
<div className="tab-list">
{tabs.map((tab, i) => (
<button key={i}>{tab.props.label}</button>
))}
</div>
<div className="tab-panels">
{children}
</div>
</div>
)
}
// Render Props - Function as children
interface DataFetcherProps<T> {
url: string
children: (data: T, loading: boolean, error: Error | null) => React.ReactNode
}
function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
// fetch logic...
return <>{children(data as T, loading, error)}</>
}
// PropsWithChildren Utility
import { PropsWithChildren } from 'react'
interface CardBaseProps {
title: string
}
function Card({ title, children }: PropsWithChildren<CardBaseProps>) {
return (
<div>
<h2>{title}</h2>
{children}
</div>
)
}
// Required vs Optional Children (pick one per component)
// Option A: Required children
// interface ContainerProps { children: React.ReactNode }
// Option B: Truly required (must provide content)
// interface ContainerProps { children: NonNullable<React.ReactNode> }
// Option C: Optional children
// interface ContainerProps { children?: React.ReactNode }Benefits:
- Correct types prevent runtime errors
- Better autocomplete and documentation
- Catches invalid children at compile time
Reference: React TypeScript Cheatsheet
Default Props Typing
Impact: CRITICAL (avoids deprecated patterns and ensures type-aware defaults)
Modern approaches to typing default props in React with TypeScript use destructuring defaults instead of the deprecated defaultProps static property.
Incorrect
// ❌ Bad
// Using deprecated defaultProps static property
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ variant, size, disabled, children }) => {
return (
<button className={`btn-${variant} btn-${size}`} disabled={disabled}>
{children}
</button>
);
};
// This pattern is deprecated and will be removed
Button.defaultProps = {
variant: 'primary',
size: 'md',
disabled: false,
};
// Default values not reflected in type system
interface CardProps {
elevation: number;
rounded: boolean;
}
function Card({ elevation, rounded }: CardProps) {
return <div style={{ boxShadow: `0 ${elevation}px ${elevation * 2}px rgba(0,0,0,0.1)` }} />;
}
Card.defaultProps = {
elevation: 2,
rounded: true,
};Problems:
defaultPropsis deprecated for function components and will be removed- TypeScript does not reflect
defaultPropsvalues in the type system - Bundlers cannot tree-shake
defaultPropseffectively
Correct
// ✅ Good
// Using default parameters in destructuring (preferred modern approach)
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
children: React.ReactNode;
}
function Button({
variant = 'primary',
size = 'md',
disabled = false,
children,
}: ButtonProps): React.ReactElement {
return (
<button className={`btn-${variant} btn-${size}`} disabled={disabled}>
{children}
</button>
);
}
// Extracting defaults for reuse and testing
interface CardProps {
elevation?: number;
rounded?: boolean;
children: React.ReactNode;
}
const cardDefaults: Required<Pick<CardProps, 'elevation' | 'rounded'>> = {
elevation: 2,
rounded: true,
};
function Card({
elevation = cardDefaults.elevation,
rounded = cardDefaults.rounded,
children,
}: CardProps): React.ReactElement {
return (
<div
className={rounded ? 'rounded' : ''}
style={{ boxShadow: `0 ${elevation}px ${elevation * 2}px rgba(0,0,0,0.1)` }}
>
{children}
</div>
);
}
// Complex default objects with proper typing
interface FormFieldProps {
name: string;
label?: string;
validation?: {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
};
styles?: {
container?: React.CSSProperties;
label?: React.CSSProperties;
input?: React.CSSProperties;
};
}
const defaultValidation: Required<NonNullable<FormFieldProps['validation']>> = {
required: false,
minLength: 0,
maxLength: Infinity,
pattern: /.*/,
};
const defaultStyles: Required<NonNullable<FormFieldProps['styles']>> = {
container: {},
label: {},
input: {},
};
function FormField({
name,
label = name,
validation = {},
styles = {},
}: FormFieldProps): React.ReactElement {
const mergedValidation = { ...defaultValidation, ...validation };
const mergedStyles = { ...defaultStyles, ...styles };
return (
<div style={mergedStyles.container}>
<label style={mergedStyles.label}>{label}</label>
<input
name={name}
required={mergedValidation.required}
minLength={mergedValidation.minLength}
maxLength={mergedValidation.maxLength}
pattern={mergedValidation.pattern.source}
style={mergedStyles.input}
/>
</div>
);
}
// Using satisfies for type-safe defaults
interface ThemeProps {
colors?: {
primary?: string;
secondary?: string;
background?: string;
};
spacing?: {
small?: number;
medium?: number;
large?: number;
};
}
const themeDefaults = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
},
spacing: {
small: 8,
medium: 16,
large: 24,
},
} satisfies Required<{
colors: Required<NonNullable<ThemeProps['colors']>>;
spacing: Required<NonNullable<ThemeProps['spacing']>>;
}>;
type Theme = {
colors: Required<NonNullable<ThemeProps['colors']>>;
spacing: Required<NonNullable<ThemeProps['spacing']>>;
};
// import { createContext } from 'react'
const defaultTheme: Theme = themeDefaults;
const ThemeContext = createContext<Theme>(defaultTheme);
function ThemeProvider({
colors = themeDefaults.colors,
spacing = themeDefaults.spacing,
children,
}: ThemeProps & { children: React.ReactNode }): React.ReactElement {
const theme = {
colors: { ...themeDefaults.colors, ...colors },
spacing: { ...themeDefaults.spacing, ...spacing },
};
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}Benefits:
- Default parameters are type-aware and TypeScript understands the values inside the function
- No deprecation concerns since
defaultPropsis deprecated for function components - Better tree-shaking by bundlers
- Explicit defaults make component behavior clear when reading the code
- Extracted default objects can be imported in tests
Reference: React TypeScript Cheatsheet
Display Name Pattern
Impact: CRITICAL (improves debugging and DevTools component identification)
Setting displayName for better debugging and DevTools integration. Components without displayName show as "Anonymous" in React DevTools, making debugging difficult.
Incorrect
// ❌ Bad
// Anonymous arrow function - shows as "Anonymous" in DevTools
export const Button = ({ children }: { children: React.ReactNode }) => {
return <button>{children}</button>;
};
// HOC without displayName - hard to identify wrapped component
function withLogger<P extends object>(Component: React.ComponentType<P>) {
return (props: P) => {
console.log('Rendering:', props);
return <Component {...props} />;
};
}
// Memo without displayName
const ExpensiveList = React.memo(({ items }: { items: string[] }) => {
return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
});
// ForwardRef without displayName
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
(props, ref) => <input ref={ref} {...props} />
);
// Context without displayName
const ThemeContext = React.createContext<'light' | 'dark'>('light');Problems:
- Anonymous components appear as "Anonymous" in React DevTools
- HOCs without displayName hide the wrapped component identity
- Error stack traces show meaningless component names
- React Profiler cannot label components for performance analysis
Correct
// ✅ Good
import React, { forwardRef, memo, createContext } from 'react';
// Named function export - automatically has displayName
export function Button({ children }: { children: React.ReactNode }): React.ReactElement {
return <button>{children}</button>;
}
// Arrow function with explicit displayName
export const IconButton = ({ icon, label }: { icon: React.ReactNode; label: string }): React.ReactElement => {
return (
<button aria-label={label}>
{icon}
</button>
);
};
IconButton.displayName = 'IconButton';
// HOC with proper displayName
function withLogger<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
const displayName = WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component';
function WithLogger(props: P) {
console.log(`Rendering ${displayName}:`, props);
return <WrappedComponent {...props} />;
}
WithLogger.displayName = `withLogger(${displayName})`;
return WithLogger;
}
const LoggedButton = withLogger(Button);
// Memo with displayName
interface ListProps {
items: string[];
onItemClick?: (item: string) => void;
}
const ExpensiveList = memo<ListProps>(({ items, onItemClick }) => {
return (
<ul>
{items.map((item) => (
<li key={item} onClick={() => onItemClick?.(item)}>
{item}
</li>
))}
</ul>
);
});
ExpensiveList.displayName = 'ExpensiveList';
// ForwardRef with displayName
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
size?: 'sm' | 'md' | 'lg';
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ size = 'md', error, className, ...rest }, ref) => {
return (
<div className="input-wrapper">
<input
ref={ref}
className={`input input-${size} ${error ? 'input-error' : ''} ${className ?? ''}`}
aria-invalid={!!error}
{...rest}
/>
{error && <span className="error">{error}</span>}
</div>
);
}
);
Input.displayName = 'Input';
// Context with displayName
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
ThemeContext.displayName = 'ThemeContext';
// Helper function for creating displayName
function getDisplayName<P>(Component: React.ComponentType<P>): string {
return Component.displayName ?? Component.name ?? 'Component';
}Benefits:
- Components appear with readable names in React DevTools
- Stack traces show meaningful component names
- Wrapped components show their hierarchy (e.g., "withLogger(Button)")
- React Profiler shows component names for performance analysis
- Component names appear in test output and snapshots
Reference: React TypeScript Cheatsheet
FC vs Function Declaration
Impact: CRITICAL (ensures consistent and future-proof component typing)
Choosing between React.FC and regular function declarations for component typing. Regular function declarations are preferred over React.FC for clarity and generic support.
Incorrect
// ❌ Bad
import React, { FC } from 'react';
interface ButtonProps {
label: string;
onClick: () => void;
}
// FC used to include children implicitly (React 17 and earlier)
// In React 18+, FC no longer includes children automatically
const Button: FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
// FC makes it harder to use generics
const List: FC<{ items: string[] }> = ({ items }) => (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);Problems:
React.FChad implicit children in React 17, causing confusion across versionsReact.FCmakes generic components awkward to typeReact.FChad issues withdefaultPropstyping- Inconsistent community conventions between React 17 and 18
Correct
// ✅ Good
// Using regular function declarations with explicit return types
interface ButtonProps {
label: string;
onClick: () => void;
}
function Button({ label, onClick }: ButtonProps): React.ReactElement {
return <button onClick={onClick}>{label}</button>;
}
// Arrow function with explicit typing
const Card = ({ title, children }: {
title: string;
children: React.ReactNode;
}): React.ReactElement => {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
};
// Generic components are cleaner without FC
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>): React.ReactElement {
return <ul>{items.map(renderItem)}</ul>;
}
// Named function export for better debugging
interface User { id: string; name: string; email: string }
export function UserProfile({ user }: { user: User }): React.ReactElement {
return <div>{user.name}</div>;
}Benefits:
- Explicit over implicit: regular functions require explicit children prop declaration
- Generic support: regular functions work better with TypeScript generics
- Consistency: avoids confusion about React 17 vs 18 FC behavior
- Better debugging: named function declarations appear with their names in DevTools
- Industry trend: the React and TypeScript communities have moved away from FC
Reference: React TypeScript Cheatsheet
ForwardRef Typing
Impact: CRITICAL (ensures ref types match actual DOM elements)
Properly typing components that forward refs to child elements. Mismatched ref types cause runtime errors or require unsafe type assertions.
Incorrect
// ❌ Bad
// Missing ref type annotation
const Input = React.forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
// Incorrect ref type
interface ButtonProps {
variant: 'primary' | 'secondary';
}
const Button = React.forwardRef<HTMLDivElement, ButtonProps>((props, ref) => {
// Ref type doesn't match the actual element
return <button ref={ref as any}>{props.variant}</button>;
});
// Not exposing ref at all for imperative handle
const ComplexInput = React.forwardRef((props: InputProps, ref) => {
const inputRef = React.useRef<HTMLInputElement>(null);
// ref is ignored, parent can't access input
return <input ref={inputRef} {...props} />;
});Problems:
- Missing type annotations result in
anytypes for ref and props - Mismatched ref element types require unsafe
as anycasts - Ignoring the forwarded ref makes the component unusable by parent components
Correct
// ✅ Good
import React, { forwardRef, useRef, useImperativeHandle } from 'react';
// Basic forwardRef with proper typing
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
label: string;
error?: string;
size?: 'sm' | 'md' | 'lg';
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, size = 'md', className, ...rest }, ref) => {
return (
<div className={`input-wrapper input-${size}`}>
<label>{label}</label>
<input
ref={ref}
className={`input ${error ? 'input-error' : ''} ${className ?? ''}`}
aria-invalid={!!error}
{...rest}
/>
{error && <span className="error-message">{error}</span>}
</div>
);
}
);
Input.displayName = 'Input';
// ForwardRef with useImperativeHandle for custom methods
interface FormInputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
validate: () => boolean;
}
interface FormInputProps {
name: string;
label: string;
required?: boolean;
pattern?: RegExp;
}
const FormInput = forwardRef<FormInputHandle, FormInputProps>(
({ name, label, required = false, pattern }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = React.useState<string | null>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
setError(null);
}
},
getValue: () => {
return inputRef.current?.value ?? '';
},
validate: () => {
const value = inputRef.current?.value ?? '';
if (required && !value) {
setError('This field is required');
return false;
}
if (pattern && !pattern.test(value)) {
setError('Invalid format');
return false;
}
setError(null);
return true;
},
}));
return (
<div className="form-input">
<label htmlFor={name}>{label}</label>
<input ref={inputRef} id={name} name={name} aria-invalid={!!error} />
{error && <span className="error">{error}</span>}
</div>
);
}
);
FormInput.displayName = 'FormInput';
// Generic forwardRef component
interface SelectOption<T> {
value: T;
label: string;
}
interface SelectProps<T> {
options: SelectOption<T>[];
value?: T;
onChange: (value: T) => void;
placeholder?: string;
}
type GenericForwardRefComponent = <T>(
props: SelectProps<T> & { ref?: React.ForwardedRef<HTMLSelectElement> }
) => React.ReactElement;
const Select: GenericForwardRefComponent = forwardRef(
<T,>(
{ options, value, onChange, placeholder }: SelectProps<T>,
ref: React.ForwardedRef<HTMLSelectElement>
) => {
return (
<select
ref={ref}
value={String(value)}
onChange={(e) => {
const selected = options.find((opt) => String(opt.value) === e.target.value);
if (selected) onChange(selected.value);
}}
>
{placeholder && <option value="">{placeholder}</option>}
{options.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</select>
);
}
) as GenericForwardRefComponent;Benefits:
- Proper typing ensures ref type matches the actual DOM element
useImperativeHandleenables custom methods with full type safety- Setting displayName improves React DevTools debugging
- Special patterns enable generic forwardRef components
- Parent components can imperatively control children safely
Reference: React TypeScript Cheatsheet
Polymorphic Component Typing
Impact: CRITICAL (enables type-safe rendering as different HTML elements)
Creating type-safe polymorphic components that can render as different HTML elements using the as prop pattern.
Incorrect
// ❌ Bad
// Using 'any' loses all type safety
interface BoxProps {
as?: any;
children?: React.ReactNode;
}
const Box = ({ as: Component = 'div', ...props }: BoxProps) => {
return <Component {...props} />;
};
// No validation of props for the rendered element
<Box as="a">This should have href</Box> // No type error, but missing required href
// String union doesn't provide prop inference
interface TextProps {
as?: 'h1' | 'h2' | 'h3' | 'p' | 'span';
children: React.ReactNode;
}
const Text = ({ as: Component = 'p', children }: TextProps) => {
return <Component>{children}</Component>;
};
// Can't pass element-specific props like htmlFor to labelProblems:
- Using
anyfor theasprop removes all type safety and autocomplete - No validation that passed props match the rendered element type
- String unions cannot infer element-specific props
Correct
// ✅ Good
import React from 'react';
// Core polymorphic types
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProps<
C extends React.ElementType,
Props = object
> = Props &
AsProp<C> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
type PolymorphicComponentPropsWithRef<
C extends React.ElementType,
Props = object
> = PolymorphicComponentProps<C, Props> & {
ref?: PolymorphicRef<C>;
};
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>['ref'];
// Simple polymorphic Box component
interface BoxOwnProps {
padding?: 'none' | 'sm' | 'md' | 'lg';
margin?: 'none' | 'sm' | 'md' | 'lg';
display?: 'block' | 'flex' | 'grid' | 'inline';
}
type BoxProps<C extends React.ElementType = 'div'> = PolymorphicComponentProps<C, BoxOwnProps>;
function Box<C extends React.ElementType = 'div'>({
as,
padding = 'none',
margin = 'none',
display = 'block',
className,
style,
...rest
}: BoxProps<C>): React.ReactElement {
const Component = as ?? 'div';
const computedStyle: React.CSSProperties = {
padding: padding !== 'none' ? `var(--spacing-${padding})` : undefined,
margin: margin !== 'none' ? `var(--spacing-${margin})` : undefined,
display,
...style,
};
return <Component className={className} style={computedStyle} {...rest} />;
}
// Usage with full type inference
<Box padding="md">Default div</Box>
<Box as="section" padding="lg">Section element</Box>
<Box as="a" href="/home">Link with href autocomplete</Box>
<Box as="button" onClick={() => {}}>Button with onClick</Box>
// Polymorphic with discriminated unions
type ButtonVariant = 'solid' | 'outline' | 'ghost';
interface ButtonBaseProps {
variant?: ButtonVariant;
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
type ButtonAsButton = ButtonBaseProps &
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof ButtonBaseProps> & {
as?: 'button';
};
type ButtonAsLink = ButtonBaseProps &
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof ButtonBaseProps> & {
as: 'a';
};
type ButtonProps = ButtonAsButton | ButtonAsLink;
function Button(props: ButtonProps): React.ReactElement {
const {
as = 'button',
variant = 'solid',
size = 'md',
isLoading = false,
className,
children,
...rest
} = props;
const classes = `btn btn-${variant} btn-${size} ${className ?? ''}`;
if (as === 'a') {
return (
<a className={classes} {...(rest as React.AnchorHTMLAttributes<HTMLAnchorElement>)}>
{isLoading ? 'Loading...' : children}
</a>
);
}
return (
<button
className={classes}
disabled={isLoading}
{...(rest as React.ButtonHTMLAttributes<HTMLButtonElement>)}
>
{isLoading ? 'Loading...' : children}
</button>
);
}
// Usage with proper type narrowing
<Button onClick={() => console.log('clicked')}>Click me</Button>
<Button as="a" href="/home">Go home</Button>Benefits:
- Full type safety: props are validated based on the rendered element
- IDE autocomplete suggests valid props for each element type
- Single component can render as any HTML element or custom component
- Properly typed refs match the rendered element
- Reduces need for wrapper components like LinkButton, SubmitButton, etc.
Reference: React TypeScript Cheatsheet
Component Props Interface
Impact: CRITICAL (ensures consistent and extensible component APIs)
Consistent typing strategy makes code predictable and maintainable. Interfaces are preferred for props because they are extendable, provide better error messages, and align with React's composition model.
Incorrect
// ❌ Bad
// Using type when interface is better
type ButtonProps = {
label: string
onClick: () => void
}
// Inline types - not reusable
function Button({ label, onClick }: { label: string; onClick: () => void }) {
return <button onClick={onClick}>{label}</button>
}
// No typing
function Card(props) {
return <div>{props.title}</div>
}Problems:
- Type aliases cannot be extended with
extendsfor composition - Inline types are not reusable across components
- Missing type annotations result in implicit
anytypes
Correct
// ✅ Good
// Interface for component props
interface ButtonProps {
label: string
onClick: () => void
disabled?: boolean
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
// Extending interfaces
interface IconButtonProps extends ButtonProps {
icon: React.ReactNode
iconPosition?: 'left' | 'right'
}
function IconButton({
label,
onClick,
disabled,
icon,
iconPosition = 'left',
}: IconButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{iconPosition === 'left' && icon}
{label}
{iconPosition === 'right' && icon}
</button>
)
}
// Extend native button props (separate component example)
interface NativeButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant: 'primary' | 'secondary'
isLoading?: boolean
}
function NativeButton({
variant,
isLoading,
children,
disabled,
...props
}: NativeButtonProps) {
return (
<button
className={`btn-${variant}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? 'Loading...' : children}
</button>
)
}
// Generic interface
interface SelectProps<T> {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
}
function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
value={getLabel(value)}
onChange={(e) => {
const selected = options.find((o) => getLabel(o) === e.target.value)
if (selected) onChange(selected)
}}
>
{options.map((option) => (
<option key={getLabel(option)} value={getLabel(option)}>
{getLabel(option)}
</option>
))}
</select>
)
}Benefits:
- Interfaces are extendable with
extendsfor composable component APIs - Better TypeScript error messages compared to type aliases
- Extending HTML element attributes gives components native prop support
- Generic interfaces enable type-safe reusable components
Reference: React TypeScript Cheatsheet
Rest Props Typing
Impact: CRITICAL (ensures type-safe prop spreading to HTML elements)
Typing rest/spread props for components that pass through HTML attributes. Proper Omit patterns prevent prop conflicts and maintain full type safety.
Incorrect
// ❌ Bad
// Using 'any' for rest props
interface ButtonProps {
variant: 'primary' | 'secondary';
[key: string]: any; // Loses all type safety
}
const Button = ({ variant, ...rest }: ButtonProps) => {
return <button className={`btn-${variant}`} {...rest} />;
};
// Not properly omitting custom props from HTML props
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error: string;
}
const Input = ({ label, error, ...rest }: InputProps) => {
// 'label' and 'error' are valid HTML attributes, causing conflicts
return (
<div>
<label>{label}</label>
<input {...rest} />
<span>{error}</span>
</div>
);
};Problems:
- Index signatures like
[key: string]: anyremove all type checking on spread props - Not omitting custom prop names that collide with HTML attributes causes conflicts
- Missing
ComponentPropsWithoutRefcan lead to ref handling issues
Correct
// ✅ Good
// Properly extending and omitting HTML attributes
interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
function Button({
variant,
size = 'md',
children,
...rest
}: ButtonProps): React.ReactElement {
return (
<button className={`btn-${variant} btn-${size}`} {...rest}>
{children}
</button>
);
}
// Using ComponentPropsWithoutRef for better ref handling
import { ComponentPropsWithoutRef } from 'react';
interface CustomInputProps {
label: string;
errorMessage?: string;
helperText?: string;
}
type InputProps = CustomInputProps & Omit<ComponentPropsWithoutRef<'input'>, keyof CustomInputProps>;
function CustomInput({
label,
errorMessage,
helperText,
id,
...rest
}: InputProps): React.ReactElement {
const inputId = id ?? `input-${label.toLowerCase().replace(/\s/g, '-')}`;
return (
<div className="form-field">
<label htmlFor={inputId}>{label}</label>
<input id={inputId} aria-invalid={!!errorMessage} {...rest} />
{errorMessage && <span className="error">{errorMessage}</span>}
{helperText && !errorMessage && <span className="helper">{helperText}</span>}
</div>
);
}
// Creating reusable base prop types
type BaseButtonProps = ComponentPropsWithoutRef<'button'>;
interface IconButtonProps extends Omit<BaseButtonProps, 'children'> {
icon: React.ReactNode;
'aria-label': string; // Required for accessibility
}
function IconButton({ icon, ...rest }: IconButtonProps): React.ReactElement {
return <button {...rest}>{icon}</button>;
}
// Generic rest props for polymorphic components
type AsProp<C extends React.ElementType> = {
as?: C;
};
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProps<
C extends React.ElementType,
Props = object
> = Props &
AsProp<C> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
interface BoxOwnProps {
padding?: 'sm' | 'md' | 'lg';
margin?: 'sm' | 'md' | 'lg';
}
type BoxProps<C extends React.ElementType = 'div'> = PolymorphicComponentProps<C, BoxOwnProps>;
function Box<C extends React.ElementType = 'div'>({
as,
padding,
margin,
className,
...rest
}: BoxProps<C>): React.ReactElement {
const Component = as ?? 'div';
const classes = [
className,
padding && `p-${padding}`,
margin && `m-${margin}`,
].filter(Boolean).join(' ');
return <Component className={classes} {...rest} />;
}
// Usage with full type safety
<Box padding="md">Content in div</Box>
<Box as="section" padding="lg">Content in section</Box>
<Box as="a" href="/home" padding="sm">Link with padding</Box>
// Spreading with explicit rest type annotation
interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
title: React.ReactNode; // Override HTML title attribute
subtitle?: string;
}
function Card({
title,
subtitle,
children,
...divProps
}: CardProps): React.ReactElement {
return (
<div {...divProps}>
<header>
<h2>{title}</h2>
{subtitle && <p>{subtitle}</p>}
</header>
<div>{children}</div>
</div>
);
}Benefits:
- Proper typing catches invalid props at compile time
- IDE shows valid HTML attributes when using the component
- Omit removes props that conflict with custom props
ComponentPropsWithoutRefproperly excludes ref from spread- Types clearly show which props are custom vs passed through
Reference: React TypeScript Cheatsheet
Create Typed Context
Impact: MEDIUM (prevents undefined context access and runtime crashes)
Context requires proper typing for both the value and the default. A common pattern is using null as default with a custom hook that throws if used outside the provider.
Incorrect
// ❌ Bad
// No typing - any is inferred
const AuthContext = createContext(undefined)
// Unsafe access - might be undefined
const AuthContext = createContext<AuthContextType | undefined>(undefined)
const auth = useContext(AuthContext)
auth.user // Error: possibly undefined
// Fake default value - misleading
const AuthContext = createContext<AuthContextType>({
user: null,
login: () => {}, // Fake implementation
logout: () => {},
})Problems:
- Untyped context loses all type safety and defaults to
any - Using
undefinedas default without a guard hook leads to unchecked access - Fake default values mask provider absence and create misleading behavior
Correct
// ✅ Good
// Null Default with Custom Hook
interface User {
id: number
name: string
email: string
}
interface AuthContextType {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isLoading: boolean
}
// Create context with null - indicates "not yet provided"
const AuthContext = createContext<AuthContextType | null>(null)
// Custom hook with runtime check
export function useAuth(): AuthContextType {
const context = useContext(AuthContext)
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context // TypeScript knows this is AuthContextType, not null
}
// Provider component
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
// assume: const authApi = createAuthApi()
const login = async (email: string, password: string) => {
setIsLoading(true)
try {
const user = await authApi.login(email, password)
setUser(user)
} finally {
setIsLoading(false)
}
}
const logout = () => {
setUser(null)
}
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)
}
// Safe usage in components
// import { Navigate } from 'react-router-dom'
function UserProfile() {
const { user, logout } = useAuth() // Typed, never null
if (!user) {
return <Navigate to="/login" />
}
return (
<div>
<h1>{user.name}</h1>
<button onClick={logout}>Logout</button>
</div>
)
}
// Multiple Contexts Pattern
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Context with Reducer
interface State {
count: number
step: number
}
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET_STEP'; payload: number }
interface CounterContextType {
state: State
dispatch: React.Dispatch<Action>
}
const CounterContext = createContext<CounterContextType | null>(null)
function counterReducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + state.step }
case 'DECREMENT':
return { ...state, count: state.count - state.step }
case 'SET_STEP':
return { ...state, step: action.payload }
default:
return state
}
}
export function useCounter() {
const context = useContext(CounterContext)
if (!context) {
throw new Error('useCounter must be used within CounterProvider')
}
return context
}
export function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(counterReducer, {
count: 0,
step: 1,
})
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
)
}Benefits:
- Type safety: context value is properly typed throughout the application
- Runtime safety: custom hook throws a clear error if used outside the provider
- No fake defaults: null indicates "not provided" clearly
- Clean API: custom hook provides a nice consumer interface
Reference: React TypeScript Cheatsheet
Typed Context Provider Pattern
Impact: MEDIUM (eliminates null checks at every useContext call site)
The recommended pattern is createContext<Type | null>(null) paired with a custom hook that throws if the context is used outside the provider. This keeps the default value honest (no dummy objects) while giving consumers a non-null type.
Incorrect
// ❌ Bad
// Fake default value — misleading and breaks if used outside provider
const AuthContext = createContext<AuthState>({
user: null,
token: "",
login: () => {}, // Dummy no-op — silently does nothing
logout: () => {}, // Dummy no-op
isAuthenticated: false,
});
// Every consumer must handle potential undefined anyway
function Profile() {
const { user } = useContext(AuthContext);
// user could still be null, but TypeScript thinks the context always exists
return <div>{user?.name}</div>;
}Problems:
- Fake default values silently do nothing when the provider is missing
- No-op functions like
login: () => {}mask bugs — the app appears to work but does nothing - No error when a component accidentally renders outside the provider tree
- Consumers get a false sense of type safety from the dummy default
Correct
// ✅ Good
// 1. Define the context type
interface AuthContextType {
user: User | null;
token: string;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
interface User {
id: string;
name: string;
email: string;
}
// 2. Create context with null default — honest about having no value
const AuthContext = createContext<AuthContextType | null>(null);
// 3. Custom hook with null check and descriptive error
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context; // Return type is AuthContextType, not AuthContextType | null
}
// 4. Provider component encapsulates state logic
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState("");
const login = async (email: string, password: string) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const data = await response.json();
setUser(data.user);
setToken(data.token);
};
const logout = () => {
setUser(null);
setToken("");
};
const value: AuthContextType = {
user,
token,
isAuthenticated: user !== null,
login,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// 5. Consumers use the custom hook — no null checks needed
function Profile() {
const { user, isAuthenticated, logout } = useAuth();
// user is User | null (from the interface), but the context itself is guaranteed
if (!isAuthenticated || !user) {
return <p>Please log in.</p>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
// 6. App wraps the tree with the provider
function App() {
return (
<AuthProvider>
<Profile />
</AuthProvider>
);
}Benefits:
createContext<T | null>(null)avoids fake defaults that mask missing providers- Custom
useAuthhook throws a clear error if used outsideAuthProvider - Consumers get
AuthContextType(notAuthContextType | null), removing redundant null checks - Provider encapsulates all state logic in one place
Reference: React TypeScript Cheatsheet — Context
Context with useReducer
Impact: MEDIUM (discriminated union actions eliminate invalid state transitions)
When combining Context with useReducer, use discriminated union actions so each action type has its own payload shape. This prevents passing the wrong payload to the wrong action.
Incorrect
// ❌ Bad
interface State {
count: number;
name: string;
items: string[];
}
// Loose action type — any action can have any payload
interface Action {
type: string;
payload: any;
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + action.payload }; // payload could be anything
case "SET_NAME":
return { ...state, name: action.payload }; // no guarantee it's a string
case "ADD_ITEM":
return { ...state, items: [...state.items, action.payload] };
default:
return state; // Unknown actions silently pass through
}
}
// Dispatching wrong payload type — not caught
// dispatch({ type: "INCREMENT", payload: "hello" })
// dispatch({ type: "SET_NAME", payload: 42 })Problems:
type: stringaccepts any string, including typos like"INCRMENT"payload: anymeans wrong payload types are never caughtdefault: return statesilently ignores unknown actions instead of failing- No connection between action type and its expected payload
Correct
// ✅ Good
// State type
interface CounterState {
count: number;
name: string;
items: string[];
isLoading: boolean;
}
// Discriminated union — each action defines its own shape
type CounterAction =
| { type: "INCREMENT" }
| { type: "DECREMENT" }
| { type: "INCREMENT_BY"; payload: number }
| { type: "SET_NAME"; payload: string }
| { type: "ADD_ITEM"; payload: string }
| { type: "REMOVE_ITEM"; payload: number }
| { type: "SET_LOADING"; payload: boolean }
| { type: "RESET" };
const initialState: CounterState = {
count: 0,
name: "",
items: [],
isLoading: false,
};
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "INCREMENT_BY":
return { ...state, count: state.count + action.payload }; // payload is number
case "SET_NAME":
return { ...state, name: action.payload }; // payload is string
case "ADD_ITEM":
return { ...state, items: [...state.items, action.payload] }; // payload is string
case "REMOVE_ITEM":
return {
...state,
items: state.items.filter((_, i) => i !== action.payload), // payload is number
};
case "SET_LOADING":
return { ...state, isLoading: action.payload }; // payload is boolean
case "RESET":
return initialState; // No payload needed
}
}
// Context setup
interface CounterContextType {
state: CounterState;
dispatch: React.Dispatch<CounterAction>;
}
const CounterContext = createContext<CounterContextType | null>(null);
function useCounter(): CounterContextType {
const context = useContext(CounterContext);
if (context === null) {
throw new Error("useCounter must be used within a CounterProvider");
}
return context;
}
function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// Consumer — dispatch is fully typed
function Counter() {
const { state, dispatch } = useCounter();
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+1</button>
<button onClick={() => dispatch({ type: "INCREMENT_BY", payload: 5 })}>+5</button>
<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
{/* Compile error: payload must be number for INCREMENT_BY */}
{/* dispatch({ type: "INCREMENT_BY", payload: "five" }) */}
{/* Compile error: INCREMENT doesn't accept payload */}
{/* dispatch({ type: "INCREMENT", payload: 1 }) */}
</div>
);
}Benefits:
- Each action type has exactly the payload shape it needs (or no payload at all)
- Wrong payload types are caught at compile time:
{ type: "INCREMENT_BY", payload: "five" }fails - Typos in action types are caught:
{ type: "INCRMENT" }fails - The reducer switch is exhaustive — no silent fallthrough on unknown actions
Reference: React TypeScript Cheatsheet — useReducer
Click Event Handler Typing
Impact: HIGH (catches wrong element types and event property access at compile time)
Properly typing click event handlers for various React elements. Using the correct event and element types provides autocomplete for event properties and catches errors.
Incorrect
// ❌ Bad
// Using 'any' for event parameter
const handleClick = (e: any) => {
console.log(e.target.value);
};
// Missing event type entirely
const onClick = (e) => {
e.preventDefault();
};
// Using wrong event type
const handleButtonClick = (e: React.MouseEvent<HTMLDivElement>) => {
// Should be HTMLButtonElement for button clicks
};
// Not handling synthetic events properly
const handleLinkClick = (e: MouseEvent) => {
// Should be React.MouseEvent, not DOM MouseEvent
e.preventDefault();
};Problems:
- Using
anyremoves all type checking on event properties - Missing type annotations result in implicit
any - Wrong element type generic means
currentTargethas wrong properties - Native
MouseEventlacks React synthetic event methods
Correct
// ✅ Good
import React, { useCallback } from 'react';
// Button click with proper typing
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked');
console.log('Button text:', event.currentTarget.textContent);
};
// Div/container click
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
console.log('Container clicked at:', event.clientX, event.clientY);
};
// Link click with preventDefault
const handleLinkClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
const href = event.currentTarget.href;
console.log('Navigating to:', href);
};
// Generic click handler for reusable components
type ClickHandler<T extends HTMLElement = HTMLElement> = (
event: React.MouseEvent<T>
) => void;
// Click with data attribute
interface ItemProps {
id: string;
name: string;
onClick: (id: string) => void;
}
function Item({ id, name, onClick }: ItemProps): React.ReactElement {
const handleClick = useCallback(
(event: React.MouseEvent<HTMLLIElement>) => {
event.stopPropagation();
onClick(id);
},
[id, onClick]
);
return <li onClick={handleClick}>{name}</li>;
}
// Click handler factory for list items
function createClickHandler<T extends { id: string }>(
item: T,
callback: (item: T) => void
): React.MouseEventHandler<HTMLElement> {
return (event) => {
event.stopPropagation();
callback(item);
};
}
// Component with optional click handler
interface CardProps {
title: string;
children: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLDivElement>;
isClickable?: boolean;
}
function Card({
title,
children,
onClick,
isClickable = !!onClick,
}: CardProps): React.ReactElement {
return (
<div
className={`card ${isClickable ? 'clickable' : ''}`}
onClick={onClick}
role={isClickable ? 'button' : undefined}
tabIndex={isClickable ? 0 : undefined}
>
<h2>{title}</h2>
{children}
</div>
);
}
// Accessible click handler (keyboard support)
function AccessibleButton({
onClick,
children,
}: {
onClick: () => void;
children: React.ReactNode;
}): React.ReactElement {
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
onClick();
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick();
}
};
return (
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{children}
</div>
);
}
// Using MouseEventHandler type alias
type ButtonClickHandler = React.MouseEventHandler<HTMLButtonElement>;
const myButtonHandler: ButtonClickHandler = (event) => {
console.log('Button:', event.currentTarget.name);
};Benefits:
- Proper event types catch errors like accessing wrong properties
- Event type should match the actual HTML element
React.MouseEventprovides synthetic event methods unlike nativeMouseEventcurrentTargetis typed whiletargetneeds assertionReact.MouseEventHandler<T>simplifies function signatures
Reference: React TypeScript Cheatsheet
Typing Form Events
Impact: HIGH (prevents runtime errors from untyped form data access)
Form events require specific types for each input element. Using any or omitting types loses all safety around e.target.value and form data access.
Incorrect
// ❌ Bad
function LoginForm() {
const [email, setEmail] = useState("");
const [role, setRole] = useState("");
// Untyped event — no safety on e.target
const handleChange = (e) => {
setEmail(e.target.value);
};
// Using 'any' — defeats TypeScript
const handleSubmit = (e: any) => {
e.preventDefault();
console.log(e.target.email.value);
};
// Wrong event type for select
const handleSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
setRole(e.target.value); // Should be HTMLSelectElement
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<select onChange={handleSelect}>
<option value="admin">Admin</option>
</select>
<button type="submit">Login</button>
</form>
);
}Problems:
anyand untyped handlers provide no autocomplete or type checking- Wrong element types (e.g.,
HTMLInputElementfor a<select>) cause silent mismatches e.targetis typed asEventTarget, not the specific element, without proper generics
Correct
// ✅ Good
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState("");
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value); // e.target is HTMLInputElement
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
};
const handleRoleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setRole(e.target.value); // e.target is HTMLSelectElement
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
email: formData.get("email") as string,
password: formData.get("password") as string,
role: formData.get("role") as string,
};
console.log("Submitting:", data);
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={email}
onChange={handleEmailChange}
/>
<input
name="password"
type="password"
value={password}
onChange={handlePasswordChange}
/>
<select name="role" value={role} onChange={handleRoleChange}>
<option value="">Select role</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
<textarea
name="notes"
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value); // e.target is HTMLTextAreaElement
}}
/>
<button type="submit">Login</button>
</form>
);
}Benefits:
- Each input element gets its correct event type (
HTMLInputElement,HTMLSelectElement,HTMLTextAreaElement) e.targetproperties are fully typed with autocompleteReact.FormEvent<HTMLFormElement>gives typed access toe.currentTargetforFormData- Compile-time errors if you access properties that don't exist on the element
Event Handler Types
Impact: HIGH (provides autocomplete for event properties and catches errors at compile time)
Event handlers in React use synthetic events with specific types. Using the correct types provides autocomplete for event properties and catches errors at compile time.
Incorrect
// ❌ Bad
// Using native Event instead of React event
const handleClick = (e: Event) => {
// Missing React-specific properties
}
// Missing element type
const handleChange = (e: React.ChangeEvent) => {
e.target.value // Error: Property 'value' does not exist
}
// Wrong event type
const handleSubmit = (e: React.MouseEvent) => { // Should be FormEvent
e.preventDefault()
}Problems:
- Native
Eventtype lacks React synthetic event properties - Missing element type generic means
targetproperties are unknown - Wrong event type does not match the actual DOM event being handled
Correct
// ✅ Good
// Mouse Events
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY)
console.log(e.currentTarget.disabled) // Button properties
}
const handleDivClick = (e: React.MouseEvent<HTMLDivElement>) => {
console.log(e.currentTarget.className)
}
<button onClick={handleClick}>Click</button>
<div onClick={handleDivClick}>Click</div>
// Form Events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
console.log(e.target.type)
console.log(e.target.checked) // For checkboxes
}
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value)
console.log(e.target.selectedOptions)
}
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value)
}
// Keyboard Events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
}
if (e.key === 'Escape') {
// close
}
console.log(e.ctrlKey, e.shiftKey, e.altKey)
}
<input onKeyDown={handleKeyDown} />
// Focus Events
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
console.log('Focused:', e.target.name)
}
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
console.log('Blurred:', e.target.value)
}
<input onFocus={handleFocus} onBlur={handleBlur} />
// Drag Events
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('text/plain', 'data')
}
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
const data = e.dataTransfer.getData('text/plain')
}
// Inline vs Defined Handlers
// Inline - types inferred automatically
<button onClick={(e) => {
// e is React.MouseEvent<HTMLButtonElement>
console.log(e.clientX)
}}>
Click
</button>
// Defined separately - must type explicitly
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX)
}
<button onClick={handleButtonClick}>Click</button>
// Typing event handler props
interface ButtonProps {
onClick?: React.MouseEventHandler<HTMLButtonElement>
onFocus?: React.FocusEventHandler<HTMLButtonElement>
}Benefits:
- Full autocomplete for event-specific properties like
clientX,key,target.value - Compile-time errors when accessing properties that do not exist on the event type
- Proper element type matching ensures
currentTargethas the correct properties React.MouseEventHandler<T>and similar aliases simplify prop typing
Reference: React TypeScript Cheatsheet
Typing Keyboard Events
Impact: HIGH (catches invalid key checks and missing modifier handling at compile time)
Keyboard events need specific element types and proper use of e.key, e.code, and modifier properties. Using any loses all safety.
Incorrect
// ❌ Bad
function SearchInput() {
// Using 'any' — no type safety on key properties
const handleKeyDown = (e: any) => {
if (e.key === "Enter") {
e.target.blur();
}
};
// Using native DOM KeyboardEvent instead of React's
const handleKeyUp = (e: KeyboardEvent) => {
console.log(e.key); // Wrong type — should be React.KeyboardEvent
};
// Untyped handler — e is implicitly 'any'
const handleKeyPress = (e) => {
if (e.keyCode === 13) {
// keyCode is deprecated
console.log("Enter pressed");
}
};
return <input onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} />;
}Problems:
anyprevents autocomplete forkey,code,metaKey,ctrlKey, etc.- Native
KeyboardEventis not the same asReact.KeyboardEventand causes type errors keyCodeis deprecated;keyandcodeare the modern standard- No element type means
e.currentTargetis untyped
Correct
// ✅ Good
function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// e.key is the logical key value ("Enter", "Escape", "a", etc.)
if (e.key === "Enter") {
e.preventDefault();
onSearch(e.currentTarget.value); // currentTarget is HTMLInputElement
}
if (e.key === "Escape") {
e.currentTarget.blur();
}
};
return <input type="search" onKeyDown={handleKeyDown} placeholder="Search..." />;
}
// Keyboard shortcuts with modifier keys
function Editor({ onSave, onUndo }: { onSave: () => void; onUndo: () => void }) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// e.metaKey = Cmd on Mac, e.ctrlKey = Ctrl on Windows/Linux
const isModifier = e.metaKey || e.ctrlKey;
if (isModifier && e.key === "s") {
e.preventDefault(); // Prevent browser save dialog
onSave();
}
if (isModifier && e.key === "z") {
e.preventDefault();
onUndo();
}
// e.code gives the physical key ("KeyS", "KeyZ", "Space")
// Useful for keyboard shortcuts that should work regardless of layout
if (e.code === "Space" && e.shiftKey) {
e.preventDefault();
console.log("Shift+Space pressed");
}
};
return <textarea onKeyDown={handleKeyDown} />;
}
// Typed handler for non-input elements
function Modal({ onClose, children }: { onClose: () => void; children: React.ReactNode }) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Escape") {
onClose();
}
};
return (
<div role="dialog" tabIndex={-1} onKeyDown={handleKeyDown}>
{children}
</div>
);
}Benefits:
React.KeyboardEvent<HTMLInputElement>provides typed access tokey,code,metaKey,ctrlKey,shiftKey,altKeye.currentTargetis properly typed as the specific HTML element- Modern
e.keyande.codeinstead of deprecatedkeyCode - Modifier key checks enable proper cross-platform keyboard shortcuts
Generic Constraints with extends
Impact: MEDIUM (prevents invalid types from being passed to generic components and functions)
Unconstrained generics accept anything, including types that lack the properties your component needs. Use extends to constrain what T can be, and keyof to constrain keys to valid property names.
Incorrect
// ❌ Bad
// Unconstrained — accepts string, number, null, anything
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
// Cannot use item.id as key — T might not have 'id'
<li key={i}>{renderItem(item)}</li>
))}
</ul>
);
}
// Unconstrained key lookup — no guarantee key exists on obj
function getProperty<T>(obj: T, key: string): unknown {
return (obj as any)[key]; // Forced to cast to any
}Problems:
Tcould be a primitive, so accessingitem.idwould crashkey: stringaccepts any string, not just valid property names ofT- Forced to use
anycasts to access properties, defeating TypeScript - No compile-time feedback when passing invalid types
Correct
// ✅ Good
// Constrain T to objects with an id
interface HasId {
id: string | number;
}
function List<T extends HasId>({
items,
renderItem,
}: {
items: T[];
renderItem: (item: T) => React.ReactNode;
}) {
return (
<ul>
{items.map((item) => (
// Safe — T is guaranteed to have .id
<li key={item.id}>{renderItem(item)}</li>
))}
</ul>
);
}
// Constrain T to record-like objects
function ObjectTable<T extends Record<string, unknown>>({
data,
keys,
}: {
data: T[];
keys: (keyof T)[];
}) {
return (
<table>
<thead>
<tr>
{keys.map((key) => (
<th key={String(key)}>{String(key)}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{keys.map((key) => (
<td key={String(key)}>{String(row[key] ?? "")}</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Constrain key to keyof T — type-safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // No cast needed — return type is T[K]
}
interface User {
id: number;
name: string;
email: string;
}
const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
const name = getProperty(user, "name"); // type is string
const id = getProperty(user, "id"); // type is number
// getProperty(user, "age"); // Compile error — "age" is not keyof User
// Multiple constraints
function merge<T extends HasId, U extends Partial<T>>(base: T, updates: U): T {
return { ...base, ...updates };
}
// Constrained component props
interface SortableListProps<T extends HasId & { sortOrder: number }> {
items: T[];
onReorder: (items: T[]) => void;
}
function SortableList<T extends HasId & { sortOrder: number }>({
items,
onReorder,
}: SortableListProps<T>) {
const sorted = [...items].sort((a, b) => a.sortOrder - b.sortOrder);
return (
<ul>
{sorted.map((item) => (
<li key={item.id}>Item {item.id} (order: {item.sortOrder})</li>
))}
</ul>
);
}Benefits:
T extends HasIdguarantees.idexists, enabling safe key extractionK extends keyof Trestricts keys to valid property names with typed return valuesT extends Record<string, unknown>ensures T is an object, not a primitive- Multiple constraints (
T extends A & B) combine requirements cleanly
Generic List Component
Impact: MEDIUM (enables reusable, type-safe list rendering for any data type)
Generic components allow building reusable, type-safe components that work with any data type. The type flows through from the data to the render function, providing full type safety.
Incorrect
// ❌ Bad
// Using any - no type safety
interface ListProps {
items: any[]
renderItem: (item: any) => React.ReactNode
}
function List({ items, renderItem }: ListProps) {
return <ul>{items.map(renderItem)}</ul>
}
// No autocomplete, no type checking
<List
items={users}
renderItem={(item) => <li>{item.nmae}</li>} // Typo not caught!
/>Problems:
- Using
anyremoves all type checking on list items - Typos in property names are not caught at compile time
- No autocomplete for item properties in the render callback
Correct
// ✅ Good
// Basic Generic List
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
keyExtractor: (item: T) => string | number
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
)
}
// Usage - T is inferred from items
interface User {
id: number
name: string
email: string
}
const users: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' }
]
<List
items={users}
renderItem={(user) => (
// user is typed as User
<span>{user.name} - {user.email}</span>
)}
keyExtractor={(user) => user.id}
/>
// With Constraints
interface HasId {
id: string | number
}
interface ConstrainedListProps<T extends HasId> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function ConstrainedList<T extends HasId>({ items, renderItem }: ConstrainedListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{renderItem(item)}
</li>
))}
</ul>
)
}
// Generic Grid Component
interface GridProps<T> {
items: T[]
columns: number
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string | number
emptyState?: React.ReactNode
}
function Grid<T>({
items,
columns,
renderItem,
keyExtractor,
emptyState = <p>No items</p>,
}: GridProps<T>) {
if (items.length === 0) {
return <>{emptyState}</>
}
return (
<div
className="grid gap-4"
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
{items.map((item) => (
<div key={keyExtractor(item)}>
{renderItem(item)}
</div>
))}
</div>
)
}
// Generic Table Component
interface Column<T> {
key: keyof T | string
header: string
render?: (item: T) => React.ReactNode
}
interface TableProps<T> {
data: T[]
columns: Column<T>[]
keyExtractor: (item: T) => string | number
}
function Table<T>({ data, columns, keyExtractor }: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={keyExtractor(item)}>
{columns.map((col) => (
<td key={String(col.key)}>
{col.render
? col.render(item)
: String(item[col.key as keyof T] ?? '')}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
// Alternative syntax: arrow function generic
// const ListArrow = <T,>({ items, renderItem }: ListProps<T>) => (
// // Note the comma after T - needed in TSX files
// <ul>{items.map(renderItem)}</ul>
// )
// Alternative syntax: with constraint
// const ListConstrained = <T extends HasId>({ items }: { items: T[] }) => (
// <ul>{items.map(item => <li key={item.id} />)}</ul>
// )Benefits:
- Full type inference from items to render callbacks
- Typos in property names are caught at compile time
- IDE autocomplete works for item properties
- Generic constraints can enforce minimum type requirements
- Reusable across any data type without losing type safety
Reference: React TypeScript Cheatsheet
Generic Select/Dropdown Component
Impact: MEDIUM (keeps value and onChange type-safe across any data type)
A generic select component lets the value type be inferred from the items array, so onChange always receives the correct type instead of forcing everything through string.
Incorrect
// ❌ Bad
interface SelectProps {
items: { label: string; value: string }[];
value: string;
onChange: (value: string) => void;
}
function Select({ items, value, onChange }: SelectProps) {
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
{items.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
);
}
// Everything forced to string — loses type information
const statusOptions = [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
];
// onChange receives string, but we wanted "active" | "inactive"
<Select items={statusOptions} value={status} onChange={(val) => setStatus(val)} />Problems:
value: stringforces all select values to be strings, even when the domain type is narroweronChangereturnsstring, requiring manual casting at every call site- No connection between the items' value type and the onChange callback type
- Cannot use numeric or object values without converting to/from strings
Correct
// ✅ Good
interface SelectOption<T> {
label: string;
value: T;
}
interface SelectProps<T> {
items: SelectOption<T>[];
value: T;
onChange: (value: T) => void;
placeholder?: string;
}
function Select<T extends string | number>({
items,
value,
onChange,
placeholder,
}: SelectProps<T>) {
return (
<select
value={String(value)}
onChange={(e) => {
const selected = items.find((item) => String(item.value) === e.target.value);
if (selected) {
onChange(selected.value); // Returns T, not string
}
}}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{items.map((item) => (
<option key={String(item.value)} value={String(item.value)}>
{item.label}
</option>
))}
</select>
);
}
// Usage — T is inferred from items
// String literal union
type Status = "active" | "inactive" | "pending";
const statusOptions: SelectOption<Status>[] = [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
{ label: "Pending", value: "pending" },
];
function StatusFilter() {
const [status, setStatus] = useState<Status>("active");
// onChange receives Status, not string
return <Select items={statusOptions} value={status} onChange={setStatus} />;
}
// Numeric values
const priorityOptions: SelectOption<number>[] = [
{ label: "Low", value: 1 },
{ label: "Medium", value: 2 },
{ label: "High", value: 3 },
];
function PriorityPicker() {
const [priority, setPriority] = useState(1);
// onChange receives number, not string
return <Select items={priorityOptions} value={priority} onChange={setPriority} />;
}Benefits:
Tis inferred from the items array, soonChangeautomatically returns the correct type- String literal unions (
"active" | "inactive") are preserved through the component - Numeric values work without string conversion at call sites
- Type mismatch between
items,value, andonChangeis caught at compile time
Generic Table Component
Impact: MEDIUM (type-safe column accessors prevent runtime data access errors)
A generic table component uses keyof T for column accessors, ensuring that column definitions can only reference properties that actually exist on the row data type.
Incorrect
// ❌ Bad
interface Column {
key: string;
header: string;
render?: (value: any, row: any) => React.ReactNode;
}
interface TableProps {
data: any[];
columns: Column[];
}
function Table({ data, columns }: TableProps) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={col.key}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map((col) => (
<td key={col.key}>
{col.render ? col.render(row[col.key], row) : row[col.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Typo in key — not caught until runtime
<Table
data={users}
columns={[
{ key: "nmae", header: "Name" }, // Typo: should be "name"
{ key: "email", header: "Email" },
]}
/>Problems:
key: stringaccepts any string, including typos and non-existent propertiesany[]data means row properties have no autocompleterendercallback receivesany, so mistakes in the render function go uncaught- No relationship between column keys and the actual data shape
Correct
// ✅ Good
interface Column<T> {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
}
interface TableProps<T> {
data: T[];
columns: Column<T>[];
keyExtractor: (row: T) => string | number;
}
function Table<T>({ data, columns, keyExtractor }: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={keyExtractor(row)}>
{columns.map((col) => (
<td key={String(col.key)}>
{col.render
? col.render(row[col.key], row)
: String(row[col.key] ?? "")}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Usage
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
const userColumns: Column<User>[] = [
{ key: "name", header: "Name" },
{ key: "email", header: "Email" },
{
key: "role",
header: "Role",
render: (value, row) => (
// row is typed as User, value is User[keyof User]
<span className={row.role === "admin" ? "font-bold" : ""}>
{String(value)}
</span>
),
},
];
function UserTable({ users }: { users: User[] }) {
return (
<Table
data={users}
columns={userColumns}
keyExtractor={(user) => user.id}
/>
);
}
// Compile error — "nmae" is not keyof User
// const badColumn: Column<User> = { key: "nmae", header: "Name" };Benefits:
key: keyof Tonly accepts property names that exist on the data type- Typos in column keys are caught at compile time, not runtime
rendercallback receives the typed row, enabling safe property access- Adding or removing fields from the data type immediately flags broken columns
Custom Hooks Typing
Impact: CRITICAL (ensures clear hook contracts with proper return types and parameters)
Creating properly typed custom hooks with clear return types and parameters. Explicit typing makes hook contracts clear to consumers and catches errors early.
Incorrect
// ❌ Bad
// No return type - callers don't know what to expect
function useUser(id) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUser(id).then(setUser).finally(() => setLoading(false));
}, [id]);
return { user, loading };
}
// Using 'any' for state
function useLocalStorage(key: string, initialValue: any) {
const [value, setValue] = useState(initialValue);
return [value, setValue];
}
// Inconsistent return type (object vs tuple)
function useToggle(initial: boolean) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(v => !v);
return { value, toggle, setValue };
}
// Missing cleanup and error handling
function useEventListener(event: string, handler: any) {
useEffect(() => {
window.addEventListener(event, handler);
}, [event, handler]);
}Problems:
- Missing parameter types result in implicit
any - No return type annotation makes the hook contract unclear
- Using
anyfor state removes all type safety - Missing effect cleanup causes memory leaks
- Missing error handling in async hooks leads to uncaught errors
Correct
// ✅ Good
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
// Hook with clear parameter and return types
interface User {
id: string;
name: string;
email: string;
}
interface UseUserResult {
user: User | null;
loading: boolean;
error: Error | null;
refetch: () => Promise<void>;
}
function useUser(userId: string | null): UseUserResult {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchUserData = useCallback(async () => {
if (!userId) {
setUser(null);
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data: User = await response.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
setUser(null);
} finally {
setLoading(false);
}
}, [userId]);
useEffect(() => {
fetchUserData();
}, [fetchUserData]);
return { user, loading, error, refetch: fetchUserData };
}
// Generic hook with type parameter
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const valueToStore = value instanceof Function ? value(prev) : value;
localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
},
[key]
);
const removeValue = useCallback(() => {
localStorage.removeItem(key);
setStoredValue(initialValue);
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
// Usage with explicit type
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
// Tuple return type for toggle hook
function useToggle(
initialValue: boolean = false
): [boolean, () => void, (value: boolean) => void] {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue((v) => !v);
}, []);
const setValueDirectly = useCallback((newValue: boolean) => {
setValue(newValue);
}, []);
return [value, toggle, setValueDirectly];
}
// Event listener hook with proper typing
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element: Window | null = window,
options?: AddEventListenerOptions
): void {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
if (!element) return;
const eventListener = (event: WindowEventMap[K]) => {
savedHandler.current(event);
};
element.addEventListener(eventName, eventListener, options);
return () => {
element.removeEventListener(eventName, eventListener, options);
};
}, [eventName, element, options]);
}
// Async hook with proper state machine
type AsyncState<T> =
| { status: 'idle'; data: null; error: null }
| { status: 'loading'; data: null; error: null }
| { status: 'success'; data: T; error: null }
| { status: 'error'; data: null; error: Error };
interface UseAsyncResult<T> {
state: AsyncState<T>;
execute: () => Promise<T | null>;
reset: () => void;
}
function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate: boolean = true
): UseAsyncResult<T> {
const [state, setState] = useState<AsyncState<T>>({
status: 'idle',
data: null,
error: null,
});
const execute = useCallback(async (): Promise<T | null> => {
setState({ status: 'loading', data: null, error: null });
try {
const data = await asyncFunction();
setState({ status: 'success', data, error: null });
return data;
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
setState({ status: 'error', data: null, error: err });
return null;
}
}, [asyncFunction]);
const reset = useCallback(() => {
setState({ status: 'idle', data: null, error: null });
}, []);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { state, execute, reset };
}
// Hook returning readonly tuple (const assertion)
function useCounter(initialValue: number = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return [count, { increment, decrement, reset }] as const;
}
// Usage - tuple is readonly [number, { increment, decrement, reset }]
const [count, { increment, decrement, reset }] = useCounter(0);Benefits:
- Explicit return types make hook contracts clear to consumers
- Generic parameters enable type-safe reuse across different data types
- Tuple returns for simple hooks, objects for complex ones
- Proper cleanup in effects prevents memory leaks
- Error states in async hooks prevent uncaught runtime errors
- Ref patterns store values that should not trigger re-renders
Reference: React TypeScript Cheatsheet
Generic Hooks Typing
Impact: CRITICAL (enables flexible, reusable hooks without losing type safety)
Creating flexible, reusable hooks with TypeScript generics. Well-designed generics preserve type information throughout the hook without requiring explicit type arguments.
Incorrect
// ❌ Bad
// Using 'any' instead of generics
function useFetch(url: string): { data: any; loading: boolean } {
const [data, setData] = useState<any>(null);
// ...
return { data, loading };
}
// Not constraining generic types when needed
function useList<T>(initial: T[]) {
const [items, setItems] = useState(initial);
const add = (item: T) => setItems([...items, item]);
const remove = (item: T) => setItems(items.filter(i => i === item)); // Needs ID
return { items, add, remove };
}
// Overly complex generics that are hard to understand
function useStore<
T extends Record<string, unknown>,
K extends keyof T,
V extends T[K],
A extends { type: string; payload: V }
>(initial: T): [T, (action: A) => void] {
// Too many type parameters
}Problems:
- Using
anyremoves all type information from hook return values - Unconstrained generics allow operations that require specific shapes (like
id) - Too many type parameters make hooks difficult to use and understand
Correct
// ✅ Good
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
// Basic generic hook for data fetching
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
interface UseFetchOptions<T> {
initialData?: T;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
transform?: (raw: unknown) => T;
}
function useFetch<T>(
url: string | null,
options: UseFetchOptions<T> = {}
): FetchState<T> & { refetch: () => Promise<void> } {
const { initialData = null, onSuccess, onError, transform } = options;
const [state, setState] = useState<FetchState<T>>({
data: initialData,
loading: !!url,
error: null,
});
const fetchData = useCallback(async () => {
if (!url) {
setState({ data: initialData, loading: false, error: null });
return;
}
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const raw = await response.json();
const data = transform ? transform(raw) : (raw as T);
setState({ data, loading: false, error: null });
onSuccess?.(data);
} catch (err) {
const error = err instanceof Error ? err : new Error('Fetch failed');
setState({ data: null, loading: false, error });
onError?.(error);
}
}, [url, initialData, transform, onSuccess, onError]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { ...state, refetch: fetchData };
}
// Usage with type inference
interface User {
id: string;
name: string;
email: string;
}
const { data: user, loading, error } = useFetch<User>('/api/user/1');
// user is User | null
// Generic list management hook with proper constraints
interface Identifiable {
id: string | number;
}
interface UseListActions<T> {
add: (item: T) => void;
remove: (id: T extends Identifiable ? T['id'] : number) => void;
update: (id: T extends Identifiable ? T['id'] : number, updates: Partial<T>) => void;
clear: () => void;
set: (items: T[]) => void;
}
function useList<T extends Identifiable>(
initialItems: T[] = []
): [T[], UseListActions<T>] {
const [items, setItems] = useState<T[]>(initialItems);
const actions: UseListActions<T> = useMemo(
() => ({
add: (item: T) => {
setItems((prev) => [...prev, item]);
},
remove: (id) => {
setItems((prev) => prev.filter((item) => item.id !== id));
},
update: (id, updates) => {
setItems((prev) =>
prev.map((item) => (item.id === id ? { ...item, ...updates } : item))
);
},
clear: () => {
setItems([]);
},
set: (newItems) => {
setItems(newItems);
},
}),
[]
);
return [items, actions];
}
// Usage
interface Todo {
id: string;
text: string;
completed: boolean;
}
const [todos, { add, remove, update }] = useList<Todo>([]);
add({ id: '1', text: 'Learn TypeScript', completed: false });
update('1', { completed: true });
remove('1');
// Generic form hook
type ValidationRule<T> = (value: T) => string | null;
interface UseFormOptions<T extends Record<string, unknown>> {
initialValues: T;
validationRules?: Partial<{ [K in keyof T]: ValidationRule<T[K]> }>;
onSubmit: (values: T) => void | Promise<void>;
}
interface UseFormResult<T extends Record<string, unknown>> {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
isSubmitting: boolean;
isValid: boolean;
handleChange: <K extends keyof T>(field: K, value: T[K]) => void;
handleBlur: <K extends keyof T>(field: K) => void;
handleSubmit: (e?: React.FormEvent) => Promise<void>;
reset: () => void;
setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;
setFieldError: <K extends keyof T>(field: K, error: string) => void;
}
function useForm<T extends Record<string, unknown>>({
initialValues,
validationRules = {},
onSubmit,
}: UseFormOptions<T>): UseFormResult<T> {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback(
<K extends keyof T>(field: K, value: T[K]): string | null => {
const rule = validationRules[field];
return rule ? rule(value) : null;
},
[validationRules]
);
const handleChange = useCallback(
<K extends keyof T>(field: K, value: T[K]) => {
setValues((prev) => ({ ...prev, [field]: value }));
if (touched[field]) {
const error = validateField(field, value);
setErrors((prev) => ({ ...prev, [field]: error ?? undefined }));
}
},
[touched, validateField]
);
const handleBlur = useCallback(
<K extends keyof T>(field: K) => {
setTouched((prev) => ({ ...prev, [field]: true }));
const error = validateField(field, values[field]);
setErrors((prev) => ({ ...prev, [field]: error ?? undefined }));
},
[values, validateField]
);
const validateAll = useCallback((): boolean => {
const newErrors: Partial<Record<keyof T, string>> = {};
let isValid = true;
for (const key of Object.keys(values) as Array<keyof T>) {
const error = validateField(key, values[key]);
if (error) {
newErrors[key] = error;
isValid = false;
}
}
setErrors(newErrors);
return isValid;
}, [values, validateField]);
const handleSubmit = useCallback(
async (e?: React.FormEvent) => {
e?.preventDefault();
if (!validateAll()) return;
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
},
[values, validateAll, onSubmit]
);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
const setFieldValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
setValues((prev) => ({ ...prev, [field]: value }));
}, []);
const setFieldError = useCallback(<K extends keyof T>(field: K, error: string) => {
setErrors((prev) => ({ ...prev, [field]: error }));
}, []);
const isValid = Object.keys(errors).length === 0;
return {
values,
errors,
touched,
isSubmitting,
isValid,
handleChange,
handleBlur,
handleSubmit,
reset,
setFieldValue,
setFieldError,
};
}
// Generic selection hook
function useSelection<T extends Identifiable>() {
const [selectedIds, setSelectedIds] = useState<Set<T['id']>>(new Set());
const select = useCallback((id: T['id']) => {
setSelectedIds((prev) => new Set(prev).add(id));
}, []);
const deselect = useCallback((id: T['id']) => {
setSelectedIds((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
}, []);
const toggle = useCallback((id: T['id']) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const isSelected = useCallback(
(id: T['id']) => selectedIds.has(id),
[selectedIds]
);
return {
selectedIds: Array.from(selectedIds),
select,
deselect,
toggle,
isSelected,
selectedCount: selectedIds.size,
};
}Benefits:
- Type safety without
any: generics preserve type information throughout the hook - Constraints using
extendslimit generic types to specific shapes - Well-designed generics often do not need explicit type arguments
- Generic hooks work with any compatible type for maximum reusability
- Clear contracts: generic return types communicate what hooks provide
Reference: React TypeScript Cheatsheet
useCallback Typing
Impact: CRITICAL (ensures memoized callbacks have correct parameter and return types)
Properly typing useCallback for memoized function references. Explicit parameter types enable autocomplete and catch errors, while proper dependencies prevent stale closures.
Incorrect
// ❌ Bad
// Missing dependency array type inference
const handleClick = useCallback((id) => {
console.log(id); // id is implicitly 'any'
}, []);
// Incorrect return type inference
const fetchData = useCallback(async () => {
const data = await api.getData();
return data; // Return type not enforced
});
// Dependencies not aligned with closure usage
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1); // Stale closure - count not in deps
}, []); // Missing count dependency
// Using 'any' for event parameter
const handleChange = useCallback((e: any) => {
setValue(e.target.value);
}, []);Problems:
- Missing parameter types result in implicit
anywith no autocomplete - Missing or incorrect dependencies cause stale closure bugs
- Using
anyfor event parameters removes all type checking - Return types are not enforced without explicit annotation
Correct
// ✅ Good
import { useCallback, useState } from 'react';
// Basic callback with explicit parameter types
const handleClick = useCallback((id: string) => {
console.log('Clicked:', id);
}, []);
// Callback with event typing
const handleInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
console.log('Input changed:', value);
},
[]
);
// Callback with return type
interface CartItem { id: string; name: string; price: number; quantity: number }
const calculateTotal = useCallback(
(items: CartItem[]): number => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
[]
);
// Async callback with proper typing
interface User {
id: string;
name: string;
email: string;
}
const fetchUser = useCallback(
async (userId: string): Promise<User> => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
},
[]
);
// Callback using state with proper dependencies
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
// Use functional update to avoid stale closure
const increment = useCallback(() => {
setCount((prevCount) => prevCount + step);
}, [step]); // Only depends on step, count uses functional update
// When you need the current value in the callback
const logAndIncrement = useCallback(() => {
console.log('Current count:', count);
setCount((prevCount) => prevCount + 1);
}, [count]); // count needed for console.log
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+{step}</button>
</div>
);
}
// Callback passed to child with proper typing
interface Item { id: string; name: string }
interface ItemListProps {
items: Item[];
onItemSelect: (item: Item) => void;
onItemDelete: (id: string) => Promise<void>;
}
function ItemContainer() {
const [items, setItems] = useState<Item[]>([]);
const handleSelect = useCallback((item: Item) => {
console.log('Selected:', item.name);
}, []);
const handleDelete = useCallback(async (id: string): Promise<void> => {
// assume api is injected or imported
await api.deleteItem(id);
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);
return (
<ItemList
items={items}
onItemSelect={handleSelect}
onItemDelete={handleDelete}
/>
);
}
// Callback with multiple parameters
interface FormData {
name: string;
email: string;
message: string;
}
type FormField = keyof FormData;
function FormComponent() {
const [formData, setFormData] = useState<Record<string, string>>({});
const handleFieldChange = useCallback(
(field: FormField, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
}
// Memoized callback for optimized child renders
const MemoizedChild = React.memo<{ onClick: () => void }>(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, MemoizedChild would re-render on every Parent render
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<MemoizedChild onClick={handleClick} />
</div>
);
}Benefits:
- Explicit parameter types enable autocomplete and catch errors
- Declared return types ensure callbacks return expected values
- Functional updates avoid stale closures without adding state to deps
- Proper React event types for form and DOM events
- Stable references prevent unnecessary child re-renders with React.memo
Reference: React TypeScript Cheatsheet
useContext Typing
Impact: CRITICAL (prevents silent failures from undefined context access)
Properly typing Context and useContext for type-safe global state. Using null defaults with custom guard hooks prevents silent failures when context is used outside its provider.
Incorrect
// ❌ Bad
// Untyped context - no type safety
const AppContext = React.createContext(undefined);
// Using 'any' for context value
const ThemeContext = React.createContext<any>({ theme: 'light' });
// Default value that doesn't match actual usage
interface User {
id: string;
name: string;
}
const UserContext = React.createContext<User>({
id: '',
name: '',
}); // Fake default encourages using context outside provider
// Not handling missing provider
const AuthContext = React.createContext<AuthContextValue | undefined>(undefined);
function useAuth() {
const context = React.useContext(AuthContext);
return context; // Could be undefined, but callers don't know
}Problems:
- Untyped context defaults to
anywith no type checking - Using
anyremoves autocomplete and error detection - Fake default values mask missing providers and create misleading behavior
- Returning potentially undefined context without a guard leaves callers unprotected
Correct
// ✅ Good
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
// Pattern 1: Context with null default and custom hook
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextValue {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextValue | null>(null);
AuthContext.displayName = 'AuthContext';
function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// assume: const api = createApiClient()
function AuthProvider({ children }: { children: React.ReactNode }): React.ReactElement {
const [user, setUser] = useState<User | null>(null);
const login = useCallback(async (email: string, password: string) => {
const response = await api.login(email, password);
setUser(response.user);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = useMemo<AuthContextValue>(
() => ({
user,
isAuthenticated: user !== null,
login,
logout,
}),
[user, login, logout]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// Usage in component
function UserProfile(): React.ReactElement {
const { user, logout } = useAuth(); // Guaranteed to be non-null
if (!user) {
return <div>Please log in</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
// Pattern 2: Multiple contexts for separation of concerns
interface ThemeContextValue {
theme: 'light' | 'dark';
colors: {
primary: string;
background: string;
text: string;
};
}
interface ThemeActionsContextValue {
setTheme: (theme: 'light' | 'dark') => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
const ThemeActionsContext = createContext<ThemeActionsContextValue | null>(null);
function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function useThemeActions(): ThemeActionsContextValue {
const context = useContext(ThemeActionsContext);
if (!context) {
throw new Error('useThemeActions must be used within ThemeProvider');
}
return context;
}
// Pattern 3: Generic context factory
function createSafeContext<T>(displayName: string) {
const Context = createContext<T | null>(null);
Context.displayName = displayName;
function useContextSafe(): T {
const context = useContext(Context);
if (context === null) {
throw new Error(`use${displayName} must be used within ${displayName}Provider`);
}
return context;
}
return [Context.Provider, useContextSafe] as const;
}
// Usage of factory
interface AppNotification { id: string; message: string; type: 'info' | 'error' | 'success' }
interface NotificationContextValue {
notifications: AppNotification[];
addNotification: (notification: Omit<AppNotification, 'id'>) => void;
removeNotification: (id: string) => void;
}
const [NotificationProvider, useNotifications] = createSafeContext<NotificationContextValue>('Notification');
// Pattern 4: Context with reducer
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR' };
interface CartState {
items: CartItem[];
total: number;
}
interface CartContextValue {
state: CartState;
dispatch: React.Dispatch<CartAction>;
itemCount: number;
addItem: (item: Omit<CartItem, 'quantity'>) => void;
removeItem: (id: string) => void;
}
const CartContext = createContext<CartContextValue | null>(null);
function useCart(): CartContextValue {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}Benefits:
- Null default with error boundary prevents silent failures
- Custom hooks encapsulate context access and error handling
- Separated contexts split state and actions to prevent unnecessary re-renders
- Factory pattern reduces boilerplate for creating typed contexts
- Memoized provider values prevent unnecessary re-renders
- DisplayName improves debugging in React DevTools
Reference: React TypeScript Cheatsheet
useMemo Typing
Impact: CRITICAL (ensures memoized values have correct types and proper dependencies)
Properly typing useMemo for memoized computed values. Explicit type annotations make code more readable, catch errors, and ensure correct dependency arrays.
Incorrect
// ❌ Bad
// Missing type annotation - relies entirely on inference
const expensiveResult = useMemo(() => {
return someExpensiveCalculation(data);
}, [data]);
// Using 'any' loses type safety
const config = useMemo<any>(() => ({
theme: 'dark',
features: ['a', 'b'],
}), []);
// Unnecessary useMemo for simple values
const double = useMemo(() => count * 2, [count]); // Simple math doesn't need memoization
// Missing dependencies causes stale values
const filteredItems = useMemo(() => {
return items.filter(item => item.category === selectedCategory);
}, []); // Missing items and selectedCategoryProblems:
- Using
anyremoves type safety on the memoized value - Missing dependencies cause stale cached values
- Simple calculations do not benefit from memoization overhead
- Unclear return types when conditionally returning different shapes
Correct
// ✅ Good
import { useMemo, useState, createContext } from 'react';
// Basic useMemo with explicit type
interface ProcessedData {
total: number;
average: number;
max: number;
min: number;
}
const statistics = useMemo<ProcessedData>(() => {
const total = numbers.reduce((sum, n) => sum + n, 0);
return {
total,
average: total / numbers.length,
max: Math.max(...numbers),
min: Math.min(...numbers),
};
}, [numbers]);
// Filtering and sorting with proper typing
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
}
type SortField = 'name' | 'price';
type SortDirection = 'asc' | 'desc';
function ProductList({ products }: { products: Product[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState<string | null>(null);
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const filteredAndSortedProducts = useMemo<Product[]>(() => {
let result = products;
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter((p) =>
p.name.toLowerCase().includes(term)
);
}
if (category) {
result = result.filter((p) => p.category === category);
}
result = [...result].sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return sortDirection === 'asc' ? comparison : -comparison;
});
return result;
}, [products, searchTerm, category, sortField, sortDirection]);
return (
<ul>
{filteredAndSortedProducts.map((product) => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
);
}
// Memoizing derived state for context
interface ThemeColors {
primary: string;
secondary: string;
background: string;
text: string;
border: string;
}
interface ThemeContextValue {
theme: 'light' | 'dark';
colors: ThemeColors;
}
const ThemeContext = createContext<ThemeContextValue>(null!);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const colors = useMemo<ThemeColors>(() => {
if (theme === 'light') {
return {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
text: '#212529',
border: '#dee2e6',
};
}
return {
primary: '#6ea8fe',
secondary: '#adb5bd',
background: '#212529',
text: '#ffffff',
border: '#495057',
};
}, [theme]);
const contextValue = useMemo<ThemeContextValue>(
() => ({ theme, colors }),
[theme, colors]
);
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}
// Memoizing expensive transformations
interface RawDataPoint {
timestamp: string;
value: number;
metadata: Record<string, unknown>;
}
interface ChartDataPoint {
x: Date;
y: number;
label: string;
}
function DataVisualization({ rawData }: { rawData: RawDataPoint[] }) {
const chartData = useMemo<ChartDataPoint[]>(() => {
return rawData.map((point) => ({
x: new Date(point.timestamp),
y: point.value,
label: `Value: ${point.value}`,
}));
}, [rawData]);
const aggregatedData = useMemo(() => {
const byMonth = new Map<string, number[]>();
chartData.forEach((point) => {
const monthKey = `${point.x.getFullYear()}-${point.x.getMonth()}`;
const existing = byMonth.get(monthKey) ?? [];
byMonth.set(monthKey, [...existing, point.y]);
});
return Array.from(byMonth.entries()).map(([month, values]) => ({
month,
average: values.reduce((a, b) => a + b, 0) / values.length,
count: values.length,
}));
}, [chartData]);
return <Chart data={chartData} aggregated={aggregatedData} />;
}
// Generic memoized selector pattern
interface Order { id: string; total: number }
function useMemoizedSelector<TState, TSelected>(
state: TState,
selector: (state: TState) => TSelected
): TSelected {
return useMemo(() => selector(state), [state, selector]);
}
// Usage
interface User {
id: string;
name: string;
email: string;
isActive: boolean;
}
interface AppState {
users: User[];
products: Product[];
orders: Order[];
}
function UserList({ state }: { state: AppState }) {
const activeUsers = useMemoizedSelector(
state,
(s) => s.users.filter((u) => u.isActive)
);
return <ul>{activeUsers.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}Benefits:
- Explicit types make code more readable and catch type mismatches
- Correct dependencies ensure memoized values stay up to date
- Only expensive calculations benefit from memoization
- Reference stability prevents unnecessary child re-renders
- Derived state avoids storing redundant data
- Memoized context values prevent provider-triggered re-renders
Reference: React TypeScript Cheatsheet
Related skills
How it compares
Pick typescript-react-patterns over generic TypeScript skills when React-specific props, hooks, refs, and generic component typing rules are the bottleneck.
FAQ
How many rules does typescript-react-patterns include?
typescript-react-patterns version 2.0.0 includes 33 rules organized into 7 categories: component typing, hook typing, event handling, ref typing, generic components, context and state, and utility types.
When should typescript-react-patterns be used?
typescript-react-patterns applies when typing React component props, custom hooks, event handlers, refs, generic reusable components, or fixing TypeScript errors in React UI code during development.