Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟢 Beginner🔵 Advanced
🎁 What are Props?📤 Passing Props to Components🔍 Reading Props in Components🎯 Props for Customization✅ Prop Types & Validation🔄 Props vs State⬇️ Passing Functions as Props🚀 Building Reusable Components
Reactjs/Props/Reading Props

🔍 Advanced Props Reading & Typing — Type-Safe Components

Advanced props reading involves mastering TypeScript type inference, generics, and patterns that create self-documenting, type-safe component contracts. These patterns prevent runtime errors and improve developer experience.


🎯 TypeScript Prop Generics and Type Inference

Generic props enable components to work with any data type while maintaining full type safety. TypeScript's inference system automatically deduces types from usage, creating flexible yet type-safe APIs.

Generics are powerful for building components that accept various data types while preserving type information throughout. This enables components to work with lists of any type, form fields for any value type, or data containers for arbitrary structures.

// Generic component with type inference
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => ReactNode;
  keyExtractor: (item: T, index: number) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item, index)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

interface User {
  id: string;
  name: string;
  email: string;
}

// TypeScript infers T = User automatically
<List<User>
  items={users}
  renderItem={(user, index) => (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  )}
  keyExtractor={(user) => user.id}
/>

<details>

<summary>📚 More Examples</summary>

// Example 2: Generic form field component
interface FormFieldProps<T> {
  value: T;
  onChange: (value: T) => void;
  validate?: (value: T) => string | null;
  children?: ReactNode;
}

function FormField<T>({
  value,
  onChange,
  validate,
  children,
}: FormFieldProps<T>) {
  const [error, setError] = useState<string | null>(null);

  const handleChange = (newValue: T) => {
    const validationError = validate?.(newValue);
    setError(validationError || null);
    if (!validationError) {
      onChange(newValue);
    }
  };

  return (
    <div>
      {children}
      {error && <span className="error">{error}</span>}
    </div>
  );
}

// Usage with different types - full type safety
<FormField<string>
  value={name}
  onChange={setName}
  validate={(v) => (v.length < 2 ? "Name too short" : null)}
>
  <input value={name} onChange={(e) => setName(e.target.value)} />
</FormField>

<FormField<number>
  value={age}
  onChange={setAge}
  validate={(v) => (v < 0 ? "Age must be positive" : null)}
>
  <input type="number" value={age} onChange={(e) => setAge(Number(e.target.value))} />
</FormField>

</details>

💡 Conditional Props and Discriminated Unions

Discriminated unions (tagged unions) in TypeScript create components with mutually exclusive props, preventing invalid prop combinations at the type level.

Discriminated unions work by having a literal type that "discriminates" between different prop combinations. The component can only receive one valid combination at a time, making impossible states impossible to represent.

// Component variant that can't receive invalid prop combinations
type ButtonSize = "small" | "medium" | "large";

// Discriminated union: either a link or button, but not both
type ButtonVariant =
  | {
      type: "button";
      onClick: () => void;
      loading?: boolean;
      disabled?: boolean;
    }
  | {
      type: "link";
      href: string;
      target?: "_blank" | "_self";
    };

interface BaseButtonProps {
  children: ReactNode;
  size?: ButtonSize;
  variant?: "primary" | "secondary" | "danger";
}

type ButtonProps = BaseButtonProps & ButtonVariant;

function Button({
  children,
  size = "medium",
  variant = "primary",
  ...rest
}: ButtonProps) {
  const sizeClasses = {
    small: "px-2 py-1 text-sm",
    medium: "px-4 py-2 text-base",
    large: "px-6 py-3 text-lg",
  };

  if (rest.type === "link") {
    return (
      <a
        href={rest.href}
        target={rest.target}
        className={`btn btn-${variant} ${sizeClasses[size]}`}
      >
        {children}
      </a>
    );
  }

  return (
    <button
      onClick={rest.onClick}
      disabled={rest.disabled || rest.loading}
      className={`btn btn-${variant} ${sizeClasses[size]}`}
    >
      {rest.loading ? "Loading..." : children}
    </button>
  );
}

// TypeScript ensures correct prop combinations
<Button type="button" onClick={() => {}} size="large">
  Click me
</Button>

<Button type="link" href="/home" target="_blank">
  Go home
</Button>

// Error: href only valid with type: "link"
// <Button type="button" href="/home" onClick={() => {}} />

<details>

<summary>📚 More Examples</summary>

// Example 2: Modal with multiple interaction modes using discriminated unions
type ModalMode =
  | {
      mode: "alert";
      action: "acknowledge";
      onAcknowledge: () => void;
    }
  | {
      mode: "confirm";
      action: "confirm_cancel";
      onConfirm: () => void;
      onCancel: () => void;
    }
  | {
      mode: "prompt";
      action: "submit_cancel";
      onSubmit: (value: string) => void;
      onCancel: () => void;
    };

interface ModalProps extends ModalMode {
  title: string;
  message: string;
  isOpen: boolean;
}

function Modal({ title, message, isOpen, ...modalProps }: ModalProps) {
  if (!isOpen) return null;

  const renderActions = () => {
    if (modalProps.mode === "alert") {
      return <button onClick={modalProps.onAcknowledge}>Acknowledge</button>;
    }
    if (modalProps.mode === "confirm") {
      return (
        <>
          <button onClick={modalProps.onConfirm}>Confirm</button>
          <button onClick={modalProps.onCancel}>Cancel</button>
        </>
      );
    }
    return null;
  };

  return (
    <div className="modal">
      <h2>{title}</h2>
      <p>{message}</p>
      <div className="actions">{renderActions()}</div>
    </div>
  );
}

</details>

🔧 Props Extraction and Utility Types

Extracting props from components enables type-safe composition and plugin architectures. TypeScript utility types like `Extract`, `Omit`, `Pick`, and conditional types create powerful patterns.

// Extract props type from component
type ComponentProps<C> = C extends React.ComponentType<infer P> ? P : never;

type InputProps = ComponentProps<"input">;

// Omit specific props
type InputPropsWithoutOnChange = Omit<InputProps, "onChange">;

// Pick specific props
type OnlyInputValueProps = Pick<InputProps, "value" | "placeholder">;

// Create enhanced component with additional props
interface EnhancedInputProps extends InputProps {
  label?: string;
  error?: string;
  helperText?: string;
}

function EnhancedInput({
  label,
  error,
  helperText,
  ...inputProps
}: EnhancedInputProps) {
  return (
    <div>
      {label && <label>{label}</label>}
      <input {...inputProps} />
      {error && <span className="error">{error}</span>}
      {helperText && <span className="helper">{helperText}</span>}
    </div>
  );
}

// Mapped types: automatically generate handler props
type HandlerProps<T extends Record<string, any>> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};

interface FormValues {
  email: string;
  password: string;
  rememberMe: boolean;
}

// Automatically generates onEmail, onPassword, onRememberMe
type FormHandlers = HandlerProps<FormValues>;

const handlers: FormHandlers = {
  onEmail: (email) => console.log(email),
  onPassword: (password) => console.log(password),
  onRememberMe: (remember) => console.log(remember),
};

<details>

<summary>📚 More Examples</summary>

// Example 2: Advanced utility type patterns
// Make all props optional except specific ones
type RequireProps<T, K extends keyof T> = Omit<T, K> &
  Required<Pick<T, K>>;

interface ComponentProps {
  title?: string;
  description?: string;
  icon?: string;
  onClick?: () => void;
}

// Require only title and description
type StrictComponentProps = RequireProps<
  ComponentProps,
  "title" | "description"
>;

const comp: StrictComponentProps = {
  title: "Hello",
  description: "World",
  // Must have title and description, icon and onClick optional
};

</details>

🎨 Runtime Props Validation with Schemas

Combine TypeScript with runtime validation libraries to create a robust safety net against type mismatches between layers, especially important when receiving data from external sources.

import { z } from "zod";

// Define schema that acts as both runtime validator and type source
const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(0).max(150),
  role: z.enum(["admin", "user", "guest"]),
});

// Extract TypeScript type from schema
type User = z.infer<typeof userSchema>;

interface UserCardProps {
  user: User; // Type-safe, validated schema
  onEdit?: (user: User) => void;
  canDelete?: boolean;
}

function UserCard({ user, onEdit, canDelete = false }: UserCardProps) {
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <p>Age: {user.age}</p>
      <p>Role: {user.role}</p>
      {onEdit && <button onClick={() => onEdit(user)}>Edit</button>}
      {canDelete && <button>Delete</button>}
    </div>
  );
}

// At runtime, validate before passing props
const rawData = await fetch("/api/user").then((r) => r.json());
const validUser = userSchema.parse(rawData); // Throws if invalid
<UserCard user={validUser} onEdit={handleEdit} />;

// Safe parsing - returns validation result instead of throwing
const result = userSchema.safeParse(rawData);
if (result.success) {
  <UserCard user={result.data} />;
} else {
  console.error("Validation errors:", result.error);
}

<details>

<summary>📚 More Examples</summary>

// Example 2: Complex nested schema validation
const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  country: z.string(),
  zipCode: z.string(),
});

const productSchema = z.object({
  id: z.string(),
  name: z.string(),
  price: z.number().positive(),
  inStock: z.boolean(),
  address: addressSchema,
  tags: z.array(z.string()).optional(),
});

type Product = z.infer<typeof productSchema>;

interface ProductFormProps {
  product: Product;
  onSubmit: (product: Product) => Promise<void>;
}

function ProductForm({ product, onSubmit }: ProductFormProps) {
  const handleSubmit = async (formData: unknown) => {
    const validated = productSchema.parse(formData);
    await onSubmit(validated);
  };

  return <form onSubmit={(e) => handleSubmit(new FormData(e.target))}>{}</form>;
}

</details>

🔑 Key Takeaways

  • ✅ Use TypeScript generics to create flexible, type-safe components
  • ✅ Discriminated unions prevent invalid prop combinations at compile time
  • ✅ Type inference reduces boilerplate while maintaining safety
  • ✅ Extract and map props for advanced composition patterns
  • ✅ Combine TypeScript with runtime validation for complete safety
  • ✅ Utility types and conditional types create powerful patterns
  • ✅ Well-typed components are self-documenting and prevent bugs

Ready to practice? Challenges | Next: Advanced Customization


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep • Build Real • Verify Skills • Launch Forward

Courses

PythonFastapiReactJSCloud

© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn