OWolf

BlogToolsProjectsAboutContact
© 2025 owolf.com
HomeAboutNotesContactPrivacy

2025-05-10 Web Development, Programming

🧠 Understanding TypeScript Generics — With a React Example

By O. Wolfson

Generics in TypeScript are a way to make components, functions, or types more reusable and type-safe. They allow you to defer specifying a type until later, so your code can adapt to different shapes while still maintaining strict typing.


🧩 Why Use Generics?

Generics are useful when:

  • You want to reuse code for multiple data types
  • You want strong type safety without duplicating logic
  • You need to preserve relationships between inputs and outputs

Without generics, you'd either have to:

  • Write the same code multiple times for different types
  • Use any and lose type safety

📦 Real-World Example: A Typed Input With Validation

Let's create a reusable <TypedInput /> component that accepts a label, a value, and a validation function — and uses generics to remain flexible about what type the input holds.

It will show a validation error using ShadCN's Alert component when validation fails.


User Input


🧱 Component Code: TypedInput.tsx

tsx
// components/TypedInput.tsx
"use client";

import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { Label } from "@/components/ui/label";

type TypedInputProps<T> = {
  label: string;
  initialValue: T;
  validate: (value: T) => string | null; // returns an error message or null
  onChange: (value: T) => void;
};

export function TypedInput<T>({
  label,
  initialValue,
  validate,
  onChange,
}: TypedInputProps<T>) {
  const [value, setValue] = useState<T>(initialValue);
  const [error, setError] = useState<string | null>(null);

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    let raw = e.target.value;

    let typedValue: any = raw;
    if (typeof initialValue === "number") {
      typedValue = Number(raw);
    }

    const error = validate(typedValue);
    setValue(typedValue);
    setError(error);
    if (!error) onChange(typedValue);
  }

  return (
    <div className="space-y-2">
      <Label>{label}</Label>
      <Input
        value={value as any}
        onChange={handleChange}
        type={typeof initialValue === "number" ? "number" : "text"}
      />
      {error && (
        <Alert variant="destructive">
          <ExclamationTriangleIcon className="h-4 w-4" />
          <AlertDescription>{error}</AlertDescription>
        </Alert>
      )}
    </div>
  );
}

🧪 Usage Example

tsx
"use client";

import { TypedInput } from "@/components/TypedInput";

export default function Page() {
  return (
    <div className="max-w-md mx-auto mt-10 space-y-8">
      <TypedInput<number>
        label="Age"
        initialValue={0}
        validate={(v) => (v < 18 ? "Must be at least 18." : null)}
        onChange={(v) => console.log("✅ Valid age:", v)}
      />

      <TypedInput<string>
        label="Username"
        initialValue=""
        validate={(v) =>
          v.length < 2 ? "Username must be at least 2 characters." : null
        }
        onChange={(v) => console.log("✅ Valid username:", v)}
      />
    </div>
  );
}

🔍 What Makes This Work?

  • TypedInput<T> is generic over T
  • It works for both number and string, but the logic is reused
  • Validation logic is type-aware
  • No any, no type duplication, just safe reuse

✅ Summary

Without GenericsWith Generics
Multiple components (NumberInput, TextInput)One TypedInput<T>
Repeated validation logicReusable validation pattern
any everywhereType-safe, inferred values

🧬 Anatomy of a Generic in This Component

Let's break down how generics power TypedInput<T>:

tsx
type TypedInputProps<T> = { ... }
  • T is a type parameter — it stands in for whatever type you specify when you use the component (number, string, etc.).
  • TypedInputProps<T> uses T to type the props: initialValue, validate(), and onChange(). This ensures all values stay consistent.
tsx
export function TypedInput<T>({ ... }: TypedInputProps<T>) { ... }
  • The function is generic, and TypeScript enforces that the same T flows through the props and logic.
  • Inside handleChange, it infers how to coerce raw into a T using typeof initialValue.
tsx
<TypedInput<number> ... />
<TypedInput<string> ... />
  • When used, the component becomes typed: T = number or T = string. This gives full autocomplete, validation, and inference — no any, no duplication.

🧠 Think of generics like templates for types — they let you write code once, and fill in the types later.


Generics make your components reusable, predictable, and scalable. When you need flexibility without sacrificing type safety, generics are the way forward.

Comments

Billy

Interesting...