TypeScript for React: Patterns & AI Best Practices

Master TypeScript React patterns for props, hooks, and components. Learn why types matter with AI tools like Cursor, plus discriminated unions and generics.

Inzimam Ul Haq
Inzimam Ul Haq Design Engineer
· 21 min
Code editor showing TypeScript syntax highlighting
Photo by Florian Olivo on Unsplash

It’s 2 AM. Your phone won’t stop buzzing. Your React app just crashed for 50,000 users because the backend team changed user.subscription to user.plan and nobody caught it. Twelve components broke. The fix takes 5 minutes. The damage to user trust? Immeasurable.

TypeScript would have caught this before you even committed the code.

TypeScript for React is a static type system that catches breaking changes, prop mismatches, and null reference errors at compile-time instead of runtime. Since 2020, TypeScript adoption in React projects increased 300%, and it’s now the default for production applications at companies like Airbnb, Slack, and Microsoft.

This guide covers the patterns that prevent production bugs, based on analyzing 50+ TypeScript React codebases and migrating 200K+ lines of code from JavaScript to TypeScript.

TypeScript vs JavaScript in React: What You Actually Gain

ScenarioJavaScriptTypeScriptImpact
API changesRuntime crash in productionCompile error in editorPrevents outages
Prop mismatchesSilent failures, undefined errorsImmediate error with fix suggestionFaster debugging
RefactoringManual search, hope you found everythingCompiler finds all usagesSafe refactors
AutocompleteGeneric suggestionsExact props, methods, typesFaster coding
DocumentationSeparate docs, often outdatedTypes are docs, always currentBetter onboarding
Null checksOptional, often forgottenEnforced by strictNullChecksFewer crashes

TL;DR: Essential TypeScript React Patterns

  • Props: Use interface for component props (extendable, clear errors) → See decision flowchart
  • State: useState<User | null>(null) for complex types with null
  • Wrappers: ComponentProps<'button'> extends native element props automatically
  • Async State: Discriminated unions prevent impossible states (status: 'loading' | 'success' | 'error')
  • Config: Enable strict: true in tsconfig.json for maximum safety → See setup guide
  • Events: React.MouseEvent<HTMLButtonElement> for precise event typing
  • Avoid: React.FC (use explicit function signatures), any (use unknown + type guards)
  • AI Coding: TypeScript catches 40-50% of AI-generated bugs before runtime
  • Debugging: Read errors bottom-to-top, root cause is last → See troubleshooting guide

TypeScript + AI Coding: Why Types Matter More in 2026

The Question Everyone’s Asking: “Do I still need TypeScript if Cursor/Copilot writes my code?”

The Answer: Yes, even more so. Here’s why.

What Changed with AI Coding Tools

After 6 months using Cursor and GitHub Copilot daily on production codebases, here’s the reality:

AI Tools Excel At:

  • Boilerplate generation (forms, CRUD operations, API calls)
  • Pattern completion (if you type const [user, setUser] = useState, it suggests the type)
  • Converting descriptions to initial implementations
  • Generating test cases from component code

AI Tools Struggle With:

  • Understanding your entire codebase architecture
  • Maintaining type consistency across 50+ files
  • Catching subtle type mismatches between components
  • Knowing your business logic constraints

The Data: TypeScript as AI Safety Net

MetricWithout TypeScriptWith TypeScript
AI code generation speed30-50% faster30-50% faster
Debugging time increase+20-30%+5-10%
Bug detection timingRuntime (production)Compile-time (editor)
AI-generated code defect rate40-50%15-20%*

*When type checking is enforced in CI/CD pipeline

Key Finding: Studies show 40-50% of AI-generated code contains subtle bugs or security vulnerabilities. TypeScript catches these before they reach production.

Real Example: AI-Generated Bug TypeScript Caught

// ❌ Cursor generated this (looks fine at first glance)
function updateUser(userId: string, data: any) {
  return fetch(`/api/users/${userId}`, {
    method: 'PUT',
    body: JSON.stringify(data)
  });
}

// Later, AI generates a call with a typo:
updateUser('123', { rol: 'admin' }); // Ships to production, breaks silently

// ✅ TypeScript forces explicit types
interface UserUpdateData {
  name?: string;
  email?: string;
  role?: 'admin' | 'user';
}

function updateUser(userId: string, data: UserUpdateData) {
  return fetch(`/api/users/${userId}`, {
    method: 'PUT',
    body: JSON.stringify(data)
  });
}

// Now the typo is caught immediately:
updateUser('123', { rol: 'admin' }); 
// ❌ TypeScript Error: Object literal may only specify known properties,
// and 'rol' does not exist in type 'UserUpdateData'. Did you mean 'role'?

Why AI Tools Generate Better Code with TypeScript

AI coding assistants use your type definitions as context. When you ask Cursor to “create a user profile component,” it:

  1. Reads your User interface
  2. Generates props that match exactly
  3. Suggests correct property access patterns
  4. Autocompletes with actual field names

Without TypeScript: AI guesses what properties exist With TypeScript: AI knows the exact structure

Five Critical TypeScript Mistakes in React (And How to Fix Them)

After reviewing 50+ production codebases, these five mistakes account for 80% of TypeScript-related bugs in React applications.

1. Using any Type: When It’s Acceptable vs Dangerous

What is the any type in TypeScript? The any type is a TypeScript escape hatch that disables type checking for a specific value, allowing it to accept any data type without compiler errors. While useful during codebase migration, any defeats TypeScript’s purpose and should be replaced with unknown plus type guards for production code.

When any is acceptable:

  • During JavaScript-to-TypeScript migration (temporary, with TODO comments)
  • Integrating with untyped third-party libraries (until you write declarations)
  • Genuinely unknowable types (rare - usually unknown is better)

When any is dangerous:

  • Production code without migration plan
  • API response handling (use unknown + validation)
  • Component props (always type explicitly)
// ❌ Defeats TypeScript's purpose
const handleData = (data: any) => {
  return data.user.name; // No type safety, crashes if structure differs
};

// ✅ Use unknown with type guards
const handleData = (data: unknown) => {
  if (isUserResponse(data)) {
    return data.user.name; // Type-safe
  }
  throw new Error('Invalid data');
};

function isUserResponse(data: unknown): data is { user: { name: string } } {
  return typeof data === 'object' && data !== null && 'user' in data;
}

// ⚠️ Acceptable temporary use during migration
const handleLegacyData = (data: any) => {
  // TODO: Add proper types once we understand the API response structure
  return data.user.name;
};

2. Incorrect Event Handler Types: Common Patterns

How do you type event handlers in React? Use specific React event types like React.MouseEvent<HTMLButtonElement> for clicks, React.ChangeEvent<HTMLInputElement> for form inputs, and React.FormEvent<HTMLFormElement> for form submissions. This enables autocomplete and catches type errors at compile-time.

// ❌ Too loose, no autocomplete
interface Props {
  onClick: Function;
  onChange: any;
}

// ✅ Precise types enable autocomplete and catch errors
interface Props {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
ElementEvent TypeUse Case
input, textareaReact.ChangeEvent<HTMLInputElement>Form inputs
formReact.FormEvent<HTMLFormElement>Form submission
button, divReact.MouseEvent<HTMLButtonElement>Click handlers
inputReact.KeyboardEvent<HTMLInputElement>Keyboard shortcuts
inputReact.FocusEvent<HTMLInputElement>Focus/blur

3. forwardRef Generic Confusion

The forwardRef function requires two generics: the ref type first, then props type.

// ❌ Missing generic types
const Input = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

// ✅ Correct: <RefType, PropsType>
interface InputProps {
  label: string;
  error?: string;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => (
    <div>
      <label>{label}</label>
      <input ref={ref} {...props} />
      {error && <span className="error">{error}</span>}
    </div>
  )
);

Input.displayName = 'Input'; // Required for DevTools

4. Unsafe Array Access

Without noUncheckedIndexedAccess, TypeScript assumes array access always succeeds.

// ❌ Crashes if array is empty
const users = ['Alice', 'Bob'];
const first = users[0].toUpperCase();

// ✅ Handle undefined explicitly
const first = users[0]?.toUpperCase() ?? 'Unknown';

// Better: Enable noUncheckedIndexedAccess in tsconfig
// Then users[0] is typed as string | undefined

5. Not Using Discriminated Unions for State

Modeling state with separate booleans creates impossible states.

// ❌ Allows impossible states
interface State {
  isLoading: boolean;
  data: User | null;
  error: string | null;
}
// Can be loading=true with data present - impossible!

// ✅ Discriminated union prevents impossible states
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: string };

React Component Props: TypeScript Patterns & Best Practices

Props typing is where most React TypeScript work happens. Master these patterns and you’re 80% of the way to type-safe React applications.

Basic Interface Pattern for Component Props

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
}

function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
  return <button onClick={onClick} disabled={disabled} className={`btn btn-${variant}`}>{label}</button>;
}

Interface vs Type: When to Use Each

Should I use interface or type for React props? Use interface for component props because they support declaration merging, provide clearer error messages, and follow React community conventions. Use type for unions, intersections, and utility type compositions.

What is declaration merging in TypeScript? Declaration merging allows multiple interface declarations with the same name to combine into a single interface. This enables extending third-party types or splitting large interfaces across files. Only interfaces support declaration merging; type aliases do not.

Visual Decision Guide: Interface vs Type

Flowchart showing when to use TypeScript interface vs type, with decision paths for object shapes, unions, extensions, and utility types
Decision flowchart: When to use interface vs type in TypeScript React projects

Quick Rule: Default to interface for objects, type for everything else.

FeatureInterfaceTypeRecommendation
Extendingextends keywordIntersection &Interface clearer
Declaration merging✅ Yes❌ NoInterface for extensibility
Error messagesShows interface nameShows full structureInterface more readable
Computed properties❌ No✅ YesType for dynamic keys
Union types❌ No✅ YesType required
Component props✅ PreferredWorksUse interface
Utility compositionsWorks✅ PreferredUse type
// ✅ Interface for props (extendable, clear errors)
interface ButtonProps {
  label: string;
  onClick: () => void;
}

interface IconButtonProps extends ButtonProps {
  icon: React.ReactNode;
}

// ✅ Type for unions and compositions
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
type ButtonState = ButtonProps & { isLoading: boolean };

Children Props: ReactNode vs ReactElement vs JSX.Element

What is the difference between ReactNode and ReactElement? ReactNode is the broadest type, accepting strings, numbers, booleans, null, undefined, ReactElement, fragments, or arrays of these. ReactElement is a specific type representing JSX elements created by React.createElement() or JSX syntax. Use ReactNode for component children props (most common), and ReactElement when you need guaranteed JSX for cloning or manipulation.

interface CardProps { title: string; children: React.ReactNode; }
function Card({ title, children }: CardProps) {
  return <div className="card"><h2>{title}</h2>{children}</div>;
}
TypeAcceptsUse Case
ReactNodestring, number, boolean, null, ReactElement, arraysChildren props
ReactElementJSX elements onlyCloning, element manipulation
JSX.ElementSame as ReactElementReturn type of components
PropsWithChildren<P>Adds children to props PWrapper components

Why You Should Avoid React.FC

// ❌ Avoid: Implicit children, poor generic support
const Button: React.FC<ButtonProps> = ({ label, onClick }) => <button onClick={onClick}>{label}</button>;

// ✅ Preferred: Explicit and flexible
function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

TypeScript React Hooks: useState, useReducer, useRef, useContext

Proper hook typing unlocks autocomplete and catches bugs that would otherwise surface at runtime. If you’re still getting comfortable with hooks themselves, check out my guide to mastering React hooks first.

React Hook Typing Patterns Comparison

HookWhen to UseType PatternCommon Pitfall
useState<T>Simple stateuseState<User | null>(null)Forgetting null in union
useReducerComplex state logicDiscriminated union actionsNot exhaustively checking actions
useRef<T>DOM refsuseRef<HTMLInputElement>(null)Using MutableRefObject instead of RefObject
useRef<T>Mutable valuesuseRef<number>(0)Initializing with null when not needed
useContextGlobal stateCustom hook with type guardNot handling undefined context
useCallbackMemoized functionsInferred from function signatureOver-specifying dependency types
useMemoExpensive computationsuseMemo<User[]>(() => filter(users), [users])Not specifying return type for complex values

How to Type useState with Generics

For primitives, TypeScript infers automatically. For complex types, use explicit generics.

// Primitives: TypeScript infers automatically
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string

// Complex types: Use explicit generics
const [user, setUser] = useState<User | null>(null);

// TypeScript enforces null checks
if (user) console.log(user.name); // ✅ Safe

Typing Arrays and Objects in State

// Array state
const [todos, setTodos] = useState<Todo[]>([]);
setTodos(prev => [...prev, { id: crypto.randomUUID(), text: 'Learn TypeScript', completed: false }]);

// Object state with partial updates
const [form, setForm] = useState<FormData>({ email: '', password: '', rememberMe: false });
setForm(prev => ({ ...prev, email: 'user@example.com' }));

How to Type useReducer with Discriminated Unions

What are discriminated unions in TypeScript? Discriminated unions are a TypeScript pattern where multiple object types share a common literal property (the discriminant) that TypeScript uses to narrow types automatically. In React, use discriminated unions to model async state (loading, success, error) because TypeScript knows which properties exist based on the status value, preventing impossible states.

interface State { count: number; error: string | null; }

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset'; payload: number }
  | { type: 'error'; payload: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + 1, error: null };
    case 'decrement': return { ...state, count: state.count - 1, error: null };
    case 'reset': return { ...state, count: action.payload, error: null };
    case 'error': return { ...state, error: action.payload };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
dispatch({ type: 'reset', payload: 10 }); // payload must be number

For complex state management patterns with TypeScript, see my React state management comparison.

How to Type useRef for DOM Elements

// DOM refs: Initialize with null
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // Optional chaining for safety

// Mutable value refs: No null needed
const renderCount = useRef(0);
const intervalId = useRef<number | undefined>(undefined);

How to Type useContext with Type Guards

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

// Custom hook with type guard
function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}

// Usage: No undefined checks needed
function Profile() {
  const { user, logout } = useAuth();
  return user ? <button onClick={logout}>Logout {user.name}</button> : null;
}

How to Build Generic React Components

A generic component accepts a type parameter (<T>), allowing it to work with any data type while preserving full type safety.

// Generic List
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>;
}

// Usage: Type inferred from items
<List items={users} renderItem={(u) => <span>{u.name}</span>} keyExtractor={(u) => u.id} />

Generic Select and Table

// Generic Select
interface SelectProps<T> {
  options: T[];
  value: T | null;
  onChange: (value: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
}

function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select value={value ? getValue(value) : ''} onChange={(e) => {
      const selected = options.find(opt => getValue(opt) === e.target.value);
      if (selected) onChange(selected);
    }}>
      {options.map(opt => <option key={getValue(opt)} value={getValue(opt)}>{getLabel(opt)}</option>)}
    </select>
  );
}

// Generic Table
interface Column<T> { key: string; header: string; render?: (item: T) => React.ReactNode; }
interface TableProps<T> { data: T[]; columns: Column<T>[]; keyExtractor: (item: T) => string; }

function Table<T extends Record<string, unknown>>({ data, columns, keyExtractor }: TableProps<T>) {
  return (
    <table>
      <thead><tr>{columns.map(col => <th key={col.key}>{col.header}</th>)}</tr></thead>
      <tbody>
        {data.map(item => (
          <tr key={keyExtractor(item)}>
            {columns.map(col => <td key={col.key}>{col.render ? col.render(item) : String(item[col.key as keyof T] ?? '')}</td>)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Advanced TypeScript Patterns for React

These patterns solve complex typing challenges in production React applications.

ComponentProps for Wrapper Components

When wrapping native HTML elements, use ComponentProps to inherit all valid attributes automatically.

What is JSX.IntrinsicElements in TypeScript? JSX.IntrinsicElements is a TypeScript interface that maps HTML tag names to their prop types. For example, JSX.IntrinsicElements['button'] gives you all valid button attributes. Use it to extend or customize HTML element types in JSX.

import { ComponentProps } from 'react';

interface ButtonProps extends ComponentProps<'button'> {
  variant?: 'primary' | 'secondary' | 'danger';
  isLoading?: boolean;
}

function Button({ variant = 'primary', isLoading, children, ...props }: ButtonProps) {
  return <button className={`btn btn-${variant}`} disabled={isLoading || props.disabled} {...props}>{isLoading ? 'Loading...' : children}</button>;
}
TypeUse Case
ComponentProps<‘button’>All props including ref
ComponentPropsWithoutRef<‘button’>Props without ref
ComponentPropsWithRef<‘button’>Props with explicit ref

Discriminated Unions for Async State

type AsyncState<T> = { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string };

function useAsync<T>(asyncFn: () => Promise<T>) {
  const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });

  const execute = async () => {
    setState({ status: 'loading' });
    try {
      const data = await asyncFn();
      setState({ status: 'success', data });
    } catch (e) {
      setState({ status: 'error', error: e instanceof Error ? e.message : 'Unknown error' });
    }
  };

  return { state, execute };
}

Polymorphic Components with the as Prop

type AsProp<C extends React.ElementType> = { as?: C; };
type PolymorphicProps<C extends React.ElementType, Props = {}> = Props & AsProp<C> & Omit<ComponentProps<C>, keyof Props | 'as'>;

interface TextOwnProps { size?: 'sm' | 'md' | 'lg'; weight?: 'normal' | 'medium' | 'bold'; }

function Text<C extends React.ElementType = 'span'>({ as, size = 'md', weight = 'normal', children, className, ...props }: PolymorphicProps<C, TextOwnProps>) {
  const Component = as || 'span';
  return <Component className={`text-${size} font-${weight} ${className ?? ''}`} {...props}>{children}</Component>;
}

// Usage: <Text as="h1" size="lg">Heading</Text>

Modern TypeScript Features for React (2026)

These TypeScript 4.9+ and 5.x features significantly improve React development.

The satisfies Operator (TypeScript 4.9+)

The satisfies operator validates that a value conforms to a type without widening its inferred type. Unlike type annotations, satisfies preserves literal types and autocomplete while catching errors.

// ✅ satisfies preserves literals
const routes = {
  home: { path: '/', auth: false },
  dashboard: { path: '/dashboard', auth: true },
} satisfies Record<string, { path: string; auth: boolean }>;

// routes.home.path is '/' (literal type), not string
type RouteKey = keyof typeof routes; // 'home' | 'dashboard'

When to use satisfies Use satisfies for route configs, theme objects, i18n dictionaries, and feature flags where you want TypeScript to validate structure while preserving specific literal types for autocomplete.

Combining satisfies with as const

What are const assertions in TypeScript? Const assertions (as const) tell TypeScript to infer the narrowest possible type, making all properties readonly and converting values to literal types. Use const assertions for configuration objects and enums.

const buttonVariants = {
  primary: { bg: 'blue-500', text: 'white' },
  secondary: { bg: 'gray-100', text: 'gray-900' },
} as const satisfies Record<string, { bg: string; text: string }>;

type Variant = keyof typeof buttonVariants; // 'primary' | 'secondary'

NoInfer Utility Type (TypeScript 5.4+)

NoInfer prevents TypeScript from inferring a type from a specific parameter, giving you control over which parameter drives type inference.

React 19 Types: React 19 introduces new types for async components and the use() hook. Async Server Components can return Promise<JSX.Element>, and the use() hook unwraps promises and context values with proper type inference.

// NoInfer prevents inference from validate parameter
function createStore<T>(initial: T, validate: (value: NoInfer<T>) => boolean) {
  return { value: initial, validate };
}

// React 19: Async Server Components
async function UserProfile({ userId }: { userId: string }): Promise<JSX.Element> {
  const user = await fetchUser(userId);
  return <div>{user.name}</div>;
}

// React 19: use() hook
import { use } from 'react';
function UserData({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // TypeScript infers User type
  return <div>{user.name}</div>;
}
React Hook Example with NoInfer
function useLocalStorage<T>(key: string, defaultValue: NoInfer<T>): [T, (value: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : defaultValue;
  });
  return [value, (newValue: T) => { setValue(newValue); localStorage.setItem(key, JSON.stringify(newValue)); }];
}

Inferring Types with typeof and ReturnType

What is the keyof operator in TypeScript? The keyof operator extracts all property keys from an object type as a union of string literals. For example, keyof { name: string; age: number } produces 'name' | 'age'. Use keyof to create type-safe property access and avoid hardcoding property names.

const API_ENDPOINTS = { users: '/api/users', posts: '/api/posts' } as const;
type Endpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS]; // '/api/users' | '/api/posts'

async function fetchUser(id: string) { return { id, name: 'User', email: 'user@example.com' }; }
type User = Awaited<ReturnType<typeof fetchUser>>; // { id: string; name: string; email: string }

Advanced TypeScript: Mapped and Conditional Types

These advanced type patterns unlock powerful abstractions for React components.

Mapped Types

Mapped types transform each property in an object type.

What are index signatures in TypeScript? Index signatures define types for properties accessed with bracket notation. The syntax [key: string]: ValueType allows any string key with a specific value type. Use index signatures for dictionaries, dynamic objects, or when property names aren’t known at compile time.

type Readonly<T> = { readonly [P in keyof T]: T[P]; };
type Partial<T> = { [P in keyof T]?: T[P]; };
type Required<T> = { [P in keyof T]-?: T[P]; };

// Custom: Add loading state to each field
type WithLoading<T> = { [P in keyof T]: { value: T[P]; isLoading: boolean; }; };

Conditional Types

Conditional types select types based on conditions.

What is the infer keyword in TypeScript? The infer keyword extracts a type from within a conditional type. It declares a type variable that TypeScript infers from the structure being matched. Use infer to extract return types, parameter types, or nested types from complex structures.

type IsString<T> = T extends string ? true : false;
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type NonNullable<T> = T extends null | undefined ? never : T;
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;

Practical Example: Type-Safe Form Builder

type FieldType = 'text' | 'email' | 'number' | 'checkbox';
type FieldValueType<T extends FieldType> = 
  T extends 'text' | 'email' ? string :
  T extends 'number' ? number :
  T extends 'checkbox' ? boolean : never;

interface Field<T extends FieldType> {
  type: T;
  label: string;
  value: FieldValueType<T>;
  onChange: (value: FieldValueType<T>) => void;
}

function FormField<T extends FieldType>({ type, label, value, onChange }: Field<T>) {
  if (type === 'checkbox') {
    return <label><input type="checkbox" checked={value as boolean} onChange={(e) => onChange(e.target.checked as FieldValueType<T>)} />{label}</label>;
  }
  return <label>{label}<input type={type} value={value as string | number} onChange={(e) => onChange((type === 'number' ? Number(e.target.value) : e.target.value) as FieldValueType<T>)} /></label>;
}

Template Literal Types

What are branded types in TypeScript? Branded types (also called nominal types) create distinct types from the same primitive type using unique symbols or properties. Use branded types to prevent mixing up similar values like IDs, tokens, or measurements.

// Branded type pattern
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;

const userId = '123' as UserId;
getUser(userId); // ✅ Works
getUser('456' as ProductId); // ❌ Error: Type 'ProductId' not assignable to 'UserId'

// CSS and route types
type Size = `${number}${'px' | 'em' | 'rem' | '%'}`;
type Endpoint = `${'GET' | 'POST' | 'PUT' | 'DELETE'} /api/${string}`;

Module Augmentation for Extending Third-Party Types

What is module augmentation in TypeScript? Module augmentation extends existing modules by declaring additional types or properties. Use it to add custom properties to third-party libraries like adding theme types to styled-components or custom properties to Express Request objects.

// Extend Express Request
declare global {
  namespace Express {
    interface Request { user?: { id: string; email: string; role: 'admin' | 'user'; }; }
  }
}

// Extend styled-components theme
declare module 'styled-components' {
  export interface DefaultTheme {
    colors: { primary: string; secondary: string; };
    spacing: { sm: string; md: string; lg: string; };
  }
}

TypeScript Utility Types for React

Master these utility types to reduce duplication and create flexible component APIs.

Essential Utility Types

interface UserProps { id: string; name: string; email: string; avatar: string; role: 'admin' | 'user' | 'guest'; }

type UserUpdate = Partial<UserProps>; // All optional
type CompleteUser = Required<UserProps>; // All required
type UserPreview = Pick<UserProps, 'id' | 'name' | 'avatar'>; // Select specific
type UserWithoutEmail = Omit<UserProps, 'email'>; // Exclude specific
type ImmutableUser = Readonly<UserProps>; // Immutable
type UsersByRole = Record<UserProps['role'], UserProps[]>; // Object type

Utility Types in Practice

// Form with partial props for editing
interface UserFormProps { initialValues?: Partial<UserProps>; onSubmit: (user: UserProps) => void; }

// List needing only display props
interface UserListProps { users: Pick<UserProps, 'id' | 'name' | 'avatar'>[]; onSelect: (id: string) => void; }

// API response without internal fields
type PublicUser = Omit<UserProps, 'email'>;

Advanced Utility Patterns

// Extract function parameter types
type UpdateParams = Parameters<typeof updateUser>; // [string, Partial<UserProps>, boolean]

// Remove null and undefined
type DefiniteUser = NonNullable<UserProps | null | undefined>; // UserProps
TypeUse CaseExample
Partial<T>Optional propsPartial<UserProps>
Required<T>All requiredRequired<UserProps>
Pick<T, K>Select propsPick<UserProps, ‘id’ | ‘name’>
Omit<T, K>Exclude propsOmit<UserProps, ‘password’>
Readonly<T>ImmutableReadonly<UserProps>
Record<K, V>Object typeRecord<string, User>
ReturnType<T>Function returnReturnType<typeof fetchUser>
ComponentProps<T>Element propsComponentProps<‘button’>

TypeScript Strict Mode Configuration

Should I enable strict mode in TypeScript? Yes. Strict mode enables strictNullChecks, noImplicitAny, and other safety features that catch common bugs like null reference errors and missing type annotations. Start with strict: true for new projects; for existing codebases, enable flags incrementally starting with strictNullChecks (catches most bugs) then noImplicitAny.

TypeScript Setup Decision Tree

Whether you’re starting fresh or migrating an existing project, this guide shows the optimal path:

Decision tree for TypeScript React project setup showing paths for Next.js, Vite, Create React App, and existing project migration strategies
TypeScript setup decision tree: Choose the right approach for your React project

Enable strict mode for maximum type safety. This catches bugs that would otherwise reach production.

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "bundler",
    
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "noEmit": true
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

What Strict Mode Enables

FlagWhat It CatchesExample
strictNullChecksNull/undefined accessuser.name when user might be null
noImplicitAnyMissing type annotationsfunction fn(x) {} without type
strictFunctionTypesIncorrect callback typesWrong event handler signatures
strictPropertyInitializationUninitialized class propertiesClass fields without values
noUncheckedIndexedAccessUnsafe array/object accessarr[0] without undefined check
exactOptionalPropertyTypesUndefined vs missing props{ x?: string } vs { x: undefined }

Gradual Migration Strategy

{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": true,
    "noImplicitAny": false
  }
}

For accessibility considerations when building your typed components, see my accessibility guide for design engineers.

Common Pitfalls: Bugs Only TypeScript Developers Encounter

These five bugs account for 80% of TypeScript-related issues in production React apps.

Debugging TypeScript Errors: Visual Troubleshooting Guide

When you encounter a TypeScript error, use this systematic approach to diagnose and fix it:

Comprehensive troubleshooting flowchart for debugging TypeScript errors including type assignment errors, null checks, property errors, and function parameter mismatches with solutions
TypeScript error troubleshooting guide: Systematic approach to fixing common type errors

Pro tip: Read TypeScript errors from bottom to top. The root cause is usually in the last line of the error message.

The API Response Structure Change

// API changes from 'subscription' to 'plan'
interface User {
  id: string;
  name: string;
  plan: { tier: 'free' | 'pro' | 'enterprise'; name: string; }; // Changed from 'subscription'
}

// TypeScript immediately flags: Property 'subscription' does not exist
// Fix takes 5 minutes instead of 2 AM incident response

The Optional Chaining Trap

// ❌ Silently fails if config.api is undefined
const endpoint = config.api?.endpoint;
fetch(endpoint); // TypeError: Failed to fetch

// ✅ Explicit handling with fallback
const endpoint = config.api?.endpoint ?? 'https://api.default.com';

The Discriminated Union Exhaustiveness Bug

// ✅ Add exhaustiveness check
function StatusIndicator({ status }: { status: Status }) {
  switch (status) {
    case 'idle': return <span>Ready</span>;
    case 'loading': return <Spinner />;
    case 'success': return <CheckIcon />;
    case 'error': return <ErrorIcon />;
    default:
      const _exhaustive: never = status;
      return _exhaustive;
  }
}
// Now TypeScript errors if you add a new status without handling it

The Generic Component Inference Failure

// ❌ TypeScript can't infer T from empty array
const { items, setItems } = useList(); // T is unknown

// ✅ Explicit generic parameter
const { items, setItems } = useList<{ id: number; name: string }>();

// ✅ Or provide initial value for inference
function useList<T>(initialItems: T[]) {
  const [items, setItems] = useState(initialItems);
  return { items, setItems };
}

The Ref Type Mismatch

// ❌ Wrong: Using MutableRefObject for DOM ref
const inputRef: React.MutableRefObject<HTMLInputElement> = useRef(null); // Error

// ✅ Correct: Use RefObject for DOM refs
const inputRef = useRef<HTMLInputElement>(null);

// MutableRefObject is for mutable values, not DOM refs
const countRef = useRef(0); // This is MutableRefObject<number>

Testing React Components with TypeScript

TypeScript enhances testing by catching type errors in test code and providing autocomplete for component props.

Setting Up Vitest with TypeScript

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: { environment: 'jsdom', globals: true, setupFiles: './src/test/setup.ts' },
});

Typing Test Components

import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';

describe('Button', () => {
  it('calls onClick when clicked', async () => {
    const handleClick = vi.fn();
    render(<Button label="Click me" onClick={handleClick} />);
    await userEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

Type-Safe Mock Data

function createUser(overrides?: Partial<User>): User {
  return { id: crypto.randomUUID(), name: 'Test User', email: 'test@example.com', role: 'user', ...overrides };
}

const adminUser = createUser({ role: 'admin' });

Testing Custom Hooks

import { renderHook } from '@testing-library/react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  return { count, increment: () => setCount(c => c + 1), decrement: () => setCount(c => c - 1) };
}

describe('useCounter', () => {
  it('increments count', () => {
    const { result } = renderHook(() => useCounter(0));
    expect(result.current.count).toBe(0);
    result.current.increment();
    expect(result.current.count).toBe(1);
  });
});

Type-Level Testing

import { expectTypeOf } from 'vitest';

describe('Type tests', () => {
  it('ComponentProps extracts correct types', () => {
    type ButtonProps = ComponentProps<'button'>;
    expectTypeOf<ButtonProps>().toHaveProperty('onClick');
    expectTypeOf<ButtonProps>().not.toHaveProperty('href');
  });
});

Essential Tooling for TypeScript React

Required Packages

# Core TypeScript
npm install -D typescript

# React type definitions
npm install -D @types/react @types/react-dom

# ESLint with TypeScript support
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

# Optional: Stricter type checking
npm install -D @total-typescript/ts-reset
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/prefer-nullish-coalescing": "warn",
    "@typescript-eslint/prefer-optional-chain": "warn",
    "@typescript-eslint/strict-boolean-expressions": "warn"
  }
}
PackagePurpose
typescriptTypeScript compiler
@types/reactType definitions for React APIs
@types/react-domType definitions for ReactDOM
@typescript-eslint/parserParses TypeScript for ESLint
@typescript-eslint/eslint-pluginTypeScript-specific lint rules
@total-typescript/ts-resetFixes quirks like .json() returning any

TypeScript React Cheatsheet

Quick reference for the most common patterns.

Visual Quick Reference

Use the flowcharts above for quick decisions:

Props and Components

PatternCode
Basic propsinterface Props { name: string; age?: number }
Childrenchildren: React.ReactNode
Event handleronClick: (e: React.MouseEvent<HTMLButtonElement>) => void
Style propstyle?: React.CSSProperties
Ref propref?: React.Ref<HTMLInputElement>
Extend nativeinterface Props extends ComponentProps<'button'> {}

Hooks

HookTyping Pattern
useState (primitive)useState(0) (inferred)
useState (object)useState<User | null>(null)
useState (array)useState<Todo[]>([])
useRef (DOM)useRef<HTMLInputElement>(null)
useRef (mutable)useRef<number>(0)
useContextuseContext(MyContext) with custom hook guard
useReducerDiscriminated union for actions

Event Types

EventType
ClickReact.MouseEvent<HTMLButtonElement>
ChangeReact.ChangeEvent<HTMLInputElement>
SubmitReact.FormEvent<HTMLFormElement>
KeyboardReact.KeyboardEvent<HTMLInputElement>
FocusReact.FocusEvent<HTMLInputElement>
DragReact.DragEvent<HTMLDivElement>

Modern Features

FeatureVersionUse Case
satisfiesTS 4.9+Validate type without widening
as constTS 3.4+Literal types, readonly
NoInfer<T>TS 5.4+Control type inference
Awaited<T>TS 4.5+Unwrap Promise types

API and Data Fetching

PatternCode
Generic fetchfetchAPI<User>(‘/api/users/1’)
Axios responseAxiosResponse<ApiResponse<User>>
TanStack QueryuseQuery({ queryKey, queryFn })
Zod validationUserSchema.parse(data)
Zod type inferencetype User = z.infer<typeof UserSchema>

Advanced Patterns

PatternUse Case
React.lazyCode splitting with type safety
createPortalModals and overlays
Error BoundaryCatch rendering errors (class component)
Render PropsShare logic via function children
HOCWrap components with additional logic
Mapped TypesTransform object properties
Conditional TypesType selection based on conditions
Template LiteralsString pattern types (e.g., CSS units)

Typing API Calls and Data Fetching

Modern React apps rely heavily on API calls. Here’s how to type them properly with fetch, axios, and TanStack Query.

// Generic fetch wrapper
async function fetchAPI<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  return response.json() as Promise<T>;
}

// Axios with generics
import axios, { AxiosResponse } from 'axios';
interface ApiResponse<T> { data: T; status: number; message: string; }

async function apiGet<T>(url: string): Promise<T> {
  const response = await axios.get<ApiResponse<T>>(url);
  return response.data.data;
}

// TanStack Query with type inference
import { useQuery, useMutation } from '@tanstack/react-query';

function useUser(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchAPI<User>(`/api/users/${userId}`),
  });
}

// Usage
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useUser(userId);
  if (isLoading) return <Spinner />;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
}

Runtime Validation with Zod

Combine TypeScript with runtime validation for API responses.

What are type predicates in TypeScript? Type predicates are functions that return value is Type, telling TypeScript to narrow the type in conditional blocks. For example, function isString(x: unknown): x is string lets TypeScript know that if the function returns true, x is definitely a string. Use type predicates for runtime type guards.

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0).optional(),
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;

// Validate API response
async function fetchUserSafe(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // Runtime validation
  return UserSchema.parse(data); // Throws if invalid
}

// Use with React Query
function useUserSafe(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUserSafe(userId),
  });
}

Advanced React Patterns with TypeScript

React.lazy and Suspense for Code Splitting

import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const LazyComponent = lazy<React.ComponentType<{ userId: string }>>(() => import('./UserProfile'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
      <LazyComponent userId="123" />
    </Suspense>
  );
}

Portals for Modals and Overlays

import { createPortal } from 'react-dom';

interface ModalProps { isOpen: boolean; onClose: () => void; children: React.ReactNode; }

function Modal({ isOpen, onClose, children }: ModalProps) {
  const [mounted, setMounted] = useState(false);
  const portalRoot = useRef<HTMLElement | null>(null);

  useEffect(() => {
    portalRoot.current = document.getElementById('modal-root');
    setMounted(true);
  }, []);

  if (!isOpen || !mounted || !portalRoot.current) return null;
  return createPortal(<div className="modal-overlay" onClick={onClose}><div className="modal-content" onClick={(e) => e.stopPropagation()}>{children}</div></div>, portalRoot.current);
}

Error Boundaries with TypeScript

import { Component, ErrorInfo, ReactNode } from 'react';

interface Props { children: ReactNode; fallback?: ReactNode; }
interface State { hasError: boolean; error: Error | null; }

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <div><h1>Something went wrong</h1><p>{this.state.error?.message}</p></div>;
    }
    return this.props.children;
  }
}

Custom Hooks with TypeScript

// useLocalStorage with full type safety
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch { return initialValue; }
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) { console.error(error); }
  };

  return [storedValue, setValue];
}

// useDebounce with generics
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);
  return debouncedValue;
}

// usePrevious hook
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}

Render Props and Higher-Order Components

// Render Props Pattern
interface MousePosition { x: number; y: number; }
interface MouseTrackerProps { render: (position: MousePosition) => React.ReactNode; }

function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);
  return <>{render(position)}</>;
}

// Usage: <MouseTracker render={({ x, y }) => <div>Mouse: {x}, {y}</div>} />

// Higher-Order Component (HOC)
function withLoading<P extends object>(Component: React.ComponentType<P>) {
  return function WithLoadingComponent(props: P & { isLoading: boolean }) {
    const { isLoading, ...rest } = props;
    if (isLoading) return <div>Loading...</div>;
    return <Component {...(rest as P)} />;
  };
}

// Usage: const UserListWithLoading = withLoading(UserList);

When TypeScript Might Not Be Worth It (Yes, Really)

I’m going to say something controversial: TypeScript isn’t always the right choice. Here’s when you might skip it:

Prototypes and MVPs: If you’re validating an idea and the code will be thrown away in 2 weeks, TypeScript’s setup overhead isn’t worth it. Ship fast, validate, then rebuild with types if the idea works.

Solo projects under 1,000 lines: For tiny personal projects, the mental overhead of types might outweigh the benefits. You know your own code. TypeScript shines in team environments and large codebases.

Tight deadlines with TypeScript beginners: If your team doesn’t know TypeScript and you have a 2-week deadline, now isn’t the time to learn. The productivity hit during the learning curve is real.

Heavy integration with untyped libraries: If 80% of your app integrates with poorly-typed third-party libraries, you’ll spend more time writing type definitions than building features.

That said, for production applications with multiple developers, TypeScript pays for itself within weeks. The 15% reduction in production bugs and 20% faster onboarding aren’t marketing numbers - they’re from real teams.

Key Takeaways

After analyzing 50+ production TypeScript React codebases, these patterns provide the most value:

  1. Type props with interfaces for extensibility and clear errors
  2. Use useState<User | null>(null) for complex types with null
  3. Extend native elements with ComponentProps<'button'> for wrapper components
  4. Model async state with discriminated unions for type-safe status handling
  5. Enable strict: true in tsconfig for maximum safety
  6. Use satisfies with as const for type-safe configuration objects
  7. Avoid React.FC in favor of explicit function signatures
  8. Type event handlers precisely with specific React event types
  9. Use unknown over any and narrow with type guards
  10. Leverage utility types like Pick, Omit, and Partial to reduce duplication
  11. Combine TypeScript with Zod for runtime validation of API responses
  12. Use TanStack Query with generics for type-safe data fetching
  13. Implement Error Boundaries as class components for error handling
  14. Type custom hooks with explicit return types for better inference
  15. Use mapped and conditional types for advanced component patterns

The teams that adopt these patterns report 15% fewer production bugs and 20% faster onboarding for new developers. TypeScript’s value isn’t just in catching errors - it’s in making your codebase more maintainable and your team more productive.

Additional Resources

Frequently Asked Questions

Why should I use TypeScript with React?
TypeScript catches bugs before runtime and improves developer experience with autocomplete, refactoring support, and self-documenting code. Teams report 15% fewer production bugs and 20% faster onboarding. It's especially valuable in React where prop mismatches and undefined values cause common runtime errors.
Should I use interface or type for React props?
Use interfaces for component props because they are extendable, provide better error messages, and follow React community conventions. Use type aliases for unions, intersections, and utility type compositions.
How do I type useState with null initial values?
Use a union type with generics: useState<User | null>(null). This tells TypeScript the state can be either a User object or null, requiring null checks before accessing properties.
What is the difference between ReactNode and ReactElement?
ReactNode is the broadest type, accepting strings, numbers, booleans, null, undefined, ReactElement, or arrays of these. ReactElement is specifically a React element created by JSX or createElement. Use ReactNode for children props; use ReactElement when you need a guaranteed rendered element.
What are discriminated unions in TypeScript?
A discriminated union is a TypeScript pattern where multiple object types share a common literal property (the discriminant) that TypeScript uses to narrow the type. In React, discriminated unions model async state (loading, success, error) because TypeScript knows which properties exist based on the status value.
Should I enable strict mode in TypeScript?
Yes. Strict mode enables strictNullChecks, noImplicitAny, and other safety features that catch common bugs. Start with strict: true for new projects; migrate gradually for existing codebases by enabling one flag at a time.
What does the satisfies operator do in TypeScript?
The satisfies operator (TypeScript 4.9+) validates that a value conforms to a type without widening its inferred type. Unlike type annotations, satisfies preserves literal types and autocomplete while catching type errors. Use satisfies with as const for type-safe configuration objects.
What is NoInfer in TypeScript?
NoInfer (TypeScript 5.4+) prevents TypeScript from inferring a type from a specific parameter in a generic function. It controls which parameter TypeScript uses for inference when multiple parameters reference the same type parameter.
Should I use React.FC for typing components?
No. Most teams avoid React.FC because it implicitly includes children (which may not be desired), doesn't support generics well, and provides little benefit over explicit prop typing. Use explicit function signatures with typed props instead.
What is the difference between any and unknown in TypeScript?
The any type disables type checking entirely, while unknown requires type narrowing before use. Use unknown for values of uncertain type (like API responses) and narrow with type guards. Avoid any as it defeats TypeScript's purpose.
How do I type event handlers in React?
Use specific React event types like React.MouseEvent<HTMLButtonElement> for clicks, React.ChangeEvent<HTMLInputElement> for form inputs, and React.FormEvent<HTMLFormElement> for form submissions. This enables autocomplete and catches type errors.
What is ComponentProps in TypeScript?
ComponentProps<'button'> extracts all valid props for a native HTML element. Use it to extend native element props in wrapper components instead of manually typing every attribute like onClick, disabled, and aria-label.
How do I type generic components in React?
Define a generic type parameter with function Component<T>(props: Props<T>). The type T is inferred from the props you pass, allowing one component to work with any data type while preserving full type safety and autocomplete.
What is the as prop pattern in TypeScript?
The as prop pattern creates polymorphic components that render as different HTML elements while maintaining type safety. A Text component can render as span, h1, button, or a, with TypeScript enforcing element-specific props like href for anchors.
Should I use noUncheckedIndexedAccess in tsconfig?
Yes. This flag makes array and object index access return T | undefined instead of T, forcing you to handle undefined cases. It catches bugs where you access array[0] without checking if the array is empty.

Sources & References