Intermediate

Using TypeScript with React

Add TypeScript to React, convert a JS app and add runtime type safety with Zod.

GitHub Repo: pkellner/pluralsight-using-typescript-with-react
Framework: Next.js · React (concurrent rendering, Suspense, Server Components, Server Functions)


Table of Contents


1. Introduction to TypeScript in a React Application

Why TypeScript?

React is 100% JavaScript, and JavaScript is a dynamically typed language: a variable’s type is determined implicitly at assignment. This can lead to unexpected behavior.

JavaScript type coercion example:

// JavaScript — surprising behavior
let result = 5 + "5"; // result: "55" (string), not 10 (number)
console.log(typeof result); // "string"

Ambiguous comparison example:

function whichIsLarger(a, b) {
  return a > b ? `${a} is larger` : `${a} is smaller`;
}

whichIsLarger(101, 1001);     // "101 is smaller" ✓
whichIsLarger("101", "1001"); // "1001 is smaller than 101" ✗ (lexicographic sort)
whichIsLarger(101, "1001");   // unpredictable per ECMAScript spec §7.2.13

TypeScript solves these problems by detecting errors at compile time, before code is even executed. Editors like VS Code leverage these types to provide IntelliSense (auto-completion, real-time error detection).


TypeScript Primitive Types

TypeScript defines three fundamental primitive types:

TypeDescriptionExample
stringText"hello world"
numberNumber (integer or decimal)42, 3.14
booleanTrue or falsetrue, false
let message: string = "Hello, TypeScript!";
let itemCount: number = 42;
let isVisible: boolean = true;

Type Annotations and Inference

TypeScript supports two complementary approaches:

1. Explicit annotation:

let name: string = "hello";
name = 1; // ✗ Error: Type 'number' is not assignable to type 'string'

2. Implicit inference (recommended for simple cases):

let name = "hello"; // TypeScript automatically infers: string
name = 1;           // ✗ Same error — TypeScript inferred the string type

Best practice: Implicit inference simplifies JavaScript → TypeScript migration. It is preferred when the type is obvious. Use explicit annotations for function parameters and component props.

The tsconfig.json file with "strict": true is essential to get the full benefit of TypeScript in React projects:

{
  "compilerOptions": {
    "strict": true,
    "jsx": "preserve",
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "moduleResolution": "bundler"
  }
}

The Three TypeScript Patterns for React

These three patterns cover the vast majority of cases encountered in React development:

mindmap
  root((TypeScript<br/>React Patterns))
    Pattern 1
      Function Parameters
      Custom Hooks
      Typing inputs/outputs
    Pattern 2
      Component Properties
      Typed props
      Interfaces / Types
    Pattern 3
      Generic Types
      useState generic
      useReducer
      createContext

2. Converting a JavaScript Application to TypeScript

Pattern 1: Function Parameters (custom hooks)

This pattern applies to any JavaScript function, but especially to React custom hooks.

Step 1 — Rename the file: use-speaker-sort-and-filter.jsuse-speaker-sort-and-filter.ts

Before (JavaScript):

function useSpeakerSortAndFilter(speakerList, speakingSaturday, speakingSunday, searchQuery) {
  return speakerList.filter(speaker =>
    (speakingSaturday ? speaker.sat : true) &&
    (speakingSunday ? speaker.sun : true) &&
    (speaker.firstName + " " + speaker.lastName)
      .toLowerCase()
      .includes(searchQuery.toLowerCase())
  );
}

After (TypeScript):

function useSpeakerSortAndFilter(
  speakerList: { sat: boolean; sun: boolean; firstName: string; lastName: string }[],
  speakingSaturday: boolean,
  speakingSunday: boolean,
  searchQuery: string
): { sat: boolean; sun: boolean; firstName: string; lastName: string }[] {
  return speakerList.filter(speaker =>
    (speakingSaturday ? speaker.sat : true) &&
    (speakingSunday ? speaker.sun : true) &&
    (speaker.firstName + " " + speaker.lastName)
      .toLowerCase()
      .includes(searchQuery.toLowerCase())
  );
}

Note: "strict": true in tsconfig.json forbids implicit any types. Every parameter must be explicitly typed.


Types and Interfaces

To simplify and reuse complex type definitions, TypeScript provides two syntaxes: type and interface.

With type:

type Speaker = {
  id: number;
  firstName: string;
  lastName: string;
  sat: boolean;
  sun: boolean;
  imageUrl: string;
  userBioShort: string;
  company: string;
  favorite: boolean;
};

With interface (functionally equivalent, extensible):

interface Speaker {
  id: number;
  firstName: string;
  lastName: string;
  sat: boolean;
  sun: boolean;
  imageUrl: string;
  userBioShort: string;
  company: string;
  favorite: boolean;
}

// Interface extension is possible:
interface ExtendedSpeaker extends Speaker {
  twitterHandle: string;
}

Simplified custom hook using the Speaker type:

function useSpeakerSortAndFilter(
  speakerList: Speaker[],
  speakingSaturday: boolean,
  speakingSunday: boolean,
  searchQuery: string
): Speaker[] {
  return speakerList.filter(speaker =>
    (speakingSaturday ? speaker.sat : true) &&
    (speakingSunday ? speaker.sun : true) &&
    `${speaker.firstName} ${speaker.lastName}`
      .toLowerCase()
      .includes(searchQuery.toLowerCase())
  );
}
Criteriontypeinterface
Syntaxtype Foo = { ... }interface Foo { ... }
ExtensionIntersection (&)extends
Declaration merging✗ Not supported✓ Supported
Union types✓ Supports A | B✗ Not directly supported
RecommendationComplex types, unionsPublic contracts, extensibility

Pattern 2: Component Properties (props)

This pattern concerns typing the props received by React components.

Rename: speaker-detail.jsxspeaker-detail.tsx

Before (JavaScript):

function SpeakerDetail({ speakerRec }) {
  return (
    <div>
      <img src={speakerRec.imageUrl} alt={speakerRec.firstName} />
      <h3>{speakerRec.firstName} {speakerRec.lastName}</h3>
      <p>{speakerRec.userBioShort}</p>
      <span>{speakerRec.company}</span>
    </div>
  );
}

After (TypeScript):

type SpeakerDetailProps = {
  speakerRec: Speaker;
};

function SpeakerDetail({ speakerRec }: SpeakerDetailProps) {
  return (
    <div>
      <img src={speakerRec.imageUrl} alt={speakerRec.firstName} />
      <h3>{speakerRec.firstName} {speakerRec.lastName}</h3>
      <p>{speakerRec.userBioShort}</p>
      <span>{speakerRec.company}</span>
    </div>
  );
}

With React.FC (alternative):

import { FC } from 'react';

type SpeakerDetailProps = {
  speakerRec: Speaker;
  onFavoriteToggle?: (id: number) => void; // optional prop
};

const SpeakerDetail: FC<SpeakerDetailProps> = ({ speakerRec, onFavoriteToggle }) => {
  return (
    <div onClick={() => onFavoriteToggle?.(speakerRec.id)}>
      <h3>{speakerRec.firstName} {speakerRec.lastName}</h3>
    </div>
  );
};

Typing children:

import { ReactNode } from 'react';

type LayoutProps = {
  children: ReactNode;
  title: string;
};

function Layout({ children, title }: LayoutProps) {
  return (
    <main>
      <h1>{title}</h1>
      {children}
    </main>
  );
}

Pattern 3: Generic Types

Generics allow reusable functions to work with different types while maintaining type safety. In React, they are pervasive with useState, useReducer, and createContext.

Generic useState

// Without initialization — union type required
const [darkTheme, setDarkTheme] = useState<boolean | undefined>();
// darkTheme: boolean | undefined
// setDarkTheme: Dispatch<SetStateAction<boolean | undefined>>

// With initialization — implicit inference (recommended)
const [darkTheme, setDarkTheme] = useState(false);
// darkTheme: boolean (automatically inferred)

// Array of objects
const [speakers, setSpeakers] = useState<Speaker[]>([]);

// Null initial value
const [errorMsg, setErrorMsg] = useState<string | null>(null);

useContext with createContext

Complex approach (when initialization is ambiguous):

type ThemeContextType = {
  darkTheme: boolean | undefined;
  toggleTheme: () => void;
};

const ThemeContext = createContext<ThemeContextType>({
  darkTheme: undefined,
  toggleTheme: () => {},
});

type ThemeProviderProps = {
  children: ReactNode;
};

function ThemeProvider({ children }: ThemeProviderProps) {
  const [darkTheme, setDarkTheme] = useState<boolean | undefined>();

  function toggleTheme() {
    setDarkTheme(prev => !prev);
  }

  return (
    <ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

Simplified approach (with initialization — implicit inference):

// Initializing to false eliminates the need to handle `undefined`
function ThemeProvider({ children }: ThemeProviderProps) {
  const [darkTheme, setDarkTheme] = useState(false); // boolean inferred

  function toggleTheme() {
    setDarkTheme(prev => !prev);
  }

  // createContext automatically infers the type from useState(false)
  const ThemeContext = createContext({
    darkTheme: false,
    toggleTheme: () => {},
  });

  return (
    <ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

Key lesson: Correctly initializing values with useState (and useReducer) radically simplifies TypeScript typing. Implicit inference often eliminates the need for union types with undefined.


Migrating ThemeProvider to TypeScript

Renaming theme-context.jsxtheme-context.tsx immediately generates three TypeScript errors:

  1. Type of the ThemeProvider component → apply the Component Properties pattern
  2. Type of the setDarkTheme setter → apply the Generic Types pattern on useState
  3. Type of createContext → declare an explicit type for the context
import { createContext, useState, ReactNode } from 'react';

type ThemeContextType = {
  darkTheme: boolean;
  toggleTheme: () => void;
};

export const ThemeContext = createContext<ThemeContextType>({
  darkTheme: false,
  toggleTheme: () => {},
});

type ThemeProviderProps = {
  children: ReactNode;
};

export function ThemeProvider({ children }: ThemeProviderProps) {
  const [darkTheme, setDarkTheme] = useState(false);

  function toggleTheme() {
    setDarkTheme(prev => !prev);
  }

  return (
    <ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

useReducer and the TypeScript Union Type

useReducer is preferable to useState when multiple interdependent states need to be managed together. TypeScript and the Union type make its usage safer and more readable.

useState vs useReducer comparison:

// === useState — simple, for independent states ===
function CounterWithState() {
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <button onClick={() => setCount(c => c - 1)}>−</button>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

// === useReducer — for interdependent states or complex logic ===
type CounterState = { count: number };
type CounterAction = { type: "increment" } | { type: "decrement" };

function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case "increment": return { count: state.count + 1 };
    case "decrement": return { count: state.count - 1 };
  }
}

function CounterWithReducer() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <button onClick={() => dispatch({ type: "decrement" })}>−</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </div>
  );
}

Discriminated Union with useReducer

The TypeScript discriminated union allows defining multiple object types sharing a discriminating property (here actionType), with different payloads depending on the action type.

Defining the discriminating type:

// /types/reducer-action-type.ts
export type ReducerActionType =
  | { actionType: "TOGGLE_FAVORITE_OPTIMISTIC"; payload: boolean }
  | { actionType: "TOGGLE_FAVORITE_REVERT"; payload: boolean }
  | { actionType: "ERROR"; payload: string }
  | { actionType: "CLEAR_ERROR" }; // no payload

Typed State and Reducer:

type State = {
  speaker: Speaker;
  error: string | null;
};

function speakerReducer(state: State, action: ReducerActionType): State {
  switch (action.actionType) {
    case "TOGGLE_FAVORITE_OPTIMISTIC":
      return {
        ...state,
        speaker: { ...state.speaker, favorite: action.payload },
      };
    case "TOGGLE_FAVORITE_REVERT":
      return {
        ...state,
        speaker: { ...state.speaker, favorite: action.payload },
      };
    case "ERROR":
      return { ...state, error: action.payload };
    case "CLEAR_ERROR":
      return { ...state, error: null };
  }
}

Component with optimistic UI:

type SpeakerFavoriteToggleProps = {
  speakerRec: Speaker;
};

function SpeakerFavoriteToggle({ speakerRec }: SpeakerFavoriteToggleProps) {
  const [state, dispatch] = useReducer(speakerReducer, {
    speaker: speakerRec,
    error: null,
  });

  async function handleFavoriteClick() {
    const newFavorite = !state.speaker.favorite;

    // Optimistic UI: immediate update
    dispatch({ actionType: "TOGGLE_FAVORITE_OPTIMISTIC", payload: newFavorite });

    try {
      await speakerFavoriteToggleAction(state.speaker);
    } catch {
      // Rollback on server error
      dispatch({ actionType: "TOGGLE_FAVORITE_REVERT", payload: !newFavorite });
      dispatch({ actionType: "ERROR", payload: "Failed to update favorite" });
    }
  }

  return (
    <button onClick={handleFavoriteClick}>
      {state.error && <span>{state.error}</span>}
      <HeartIcon filled={state.speaker.favorite} />
    </button>
  );
}

useRef and DOM Manipulation

useRef allows direct access to DOM nodes. TypeScript enforces the reference type and ensures consistency between initialization and usage.

Two main use cases:

  1. Store data without triggering a re-render (render counters, timers)
  2. Directly access a DOM node (auto-centering, focus, scroll)
import { useRef, useCallback } from 'react';

// Case 1: Render counter (does not cause re-render)
function RenderCounter() {
  const renderCount = useRef<number>(0);
  renderCount.current += 1;

  return <div>Render count: {renderCount.current}</div>;
}

// Case 2: Reference to a DOM element for auto-scrolling
function SpeakerRow({ speaker }: { speaker: Speaker }) {
  const rowRef = useRef<HTMLDivElement>(null);

  const handleClick = useCallback(() => {
    if (rowRef.current) {
      rowRef.current.scrollIntoView({
        behavior: "smooth",
        block: "center",
      });
    }
  }, []);

  return (
    <div ref={rowRef} onClick={handleClick}>
      <h3>{speaker.firstName} {speaker.lastName}</h3>
    </div>
  );
}

useRef types by use case:

// Mutable reference (internal data — not tied to the DOM)
const renderCount = useRef<number>(0);
// renderCount.current is freely modifiable

// DOM reference — initialized to null (React will assign value on mount)
const divRef = useRef<HTMLDivElement>(null);
// divRef.current is readonly from TypeScript's perspective

// DOM reference — HTMLInputElement for an input field
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // optional chaining for safety

3. Runtime Type Safety — Zod and React Server Components

Why Runtime Validation?

TypeScript is a transpiler: it produces JavaScript and no longer exists at runtime. It guarantees type safety at code-writing time, but not when data arrives over a network.

LayerTypeScriptZod
Check timingCompile time (edit)Runtime
Network data✗ Not verified✓ Verified and parsed
Form data✗ Not verified✓ Verified and parsed
VS Code IntelliSense✓ (via z.infer)
TypeScript outputStatic typesTypes + runtime validation

Typical cases where Zod is necessary:

  • Data received from a REST API (GET speakers)
  • Data sent to a Server Function (React Server Functions)
  • Passing data between React Server Components and Client Components (crossing the network boundary)

Creating a Zod Schema

Installation:

npm install zod

Schema definition and TypeScript type (DRY — Don’t Repeat Yourself):

// /types/speaker.ts
import { z } from "zod";

// Zod schema — defines the runtime validation rules
export const speakerSchema = z.object({
  id: z.number().int().positive(),
  firstName: z.string().min(1).max(100),
  lastName: z.string().min(1).max(100),
  sat: z.boolean(),
  sun: z.boolean(),
  imageUrl: z.string().url(),
  userBioShort: z.string().max(500),
  company: z.string(),
  favorite: z.boolean(),
});

// TypeScript type inferred from the Zod schema — avoids duplication
export type Speaker = z.infer<typeof speakerSchema>;
// Equivalent to:
// type Speaker = {
//   id: number;
//   firstName: string;
//   ...
// }

Zod validation methods:

// parse — throws an exception if data is invalid
const validSpeaker = speakerSchema.parse(rawData);

// safeParse — returns { success, data } or { success: false, error }
const result = speakerSchema.safeParse(rawData);
if (result.success) {
  console.log(result.data); // typed Speaker
} else {
  console.error(result.error.issues); // error details
}

// For an array of speakers
const validSpeakers = speakerSchema.array().parse(rawDataArray);

Server-side Validation (React Server Component)

// /app/speakers-page.tsx — React Server Component (runs on Node.js)
import { speakerSchema } from "@/types/speaker";

async function SpeakersPage() {
  // Fetch data from the source
  const rawData = await loadSpeakersData();

  // Runtime validation with Zod — throws an error if data is invalid
  const speakersData = speakerSchema.array().parse(rawData);
  // speakersData is now guaranteed as valid Speaker[]

  return (
    <SpeakersDataProvider initialData={speakersData}>
      <SpeakersList />
    </SpeakersDataProvider>
  );
}

Client-side Validation (React Client Component)

// /app/speakers-list.tsx — React Client Component (runs in the browser)
"use client";

import { speakerSchema } from "@/types/speaker";
import { useSpeakerSortAndFilter } from "@/hooks/use-speaker-sort-and-filter";

function SpeakersList() {
  const { speakerList } = useContext(SpeakersDataContext);

  // Runtime validation in the browser — same Zod schema on both client and server
  const validatedSpeakers = speakerSchema.array().parse(speakerList);
  // Data has crossed a network boundary — validation is essential

  const filteredSpeakers = useSpeakerSortAndFilter(
    validatedSpeakers,
    speakingSaturday,
    speakingSunday,
    searchQuery
  );

  return (
    <div>
      {filteredSpeakers.map(speaker => (
        <SpeakerDetail key={speaker.id} speakerRec={speaker} />
      ))}
    </div>
  );
}

Validation in a React Server Function

// /actions/speaker-favorite-toggle-action.ts
// Rename: .js → .ts
import { Speaker, speakerSchema } from "@/types/speaker";

export async function speakerFavoriteToggleAction(speakerRec: Speaker) {
  // Runtime validation at the Server Function entry point
  // Data comes from the browser — network boundary crossed
  const validatedSpeaker = speakerSchema.parse(speakerRec);
  // If invalid, a ZodError is automatically thrown

  // Update in the database
  await updateSpeakerFavorite(validatedSpeaker.id, validatedSpeaker.favorite);

  return { success: true };
}

Reference Diagrams

TypeScript Type System for React

graph TD
    A[TypeScript Types] --> B[Primitives]
    A --> C[Composite Types]
    A --> D[Generics]
    A --> E[Union Types]

    B --> B1[string]
    B --> B2[number]
    B --> B3[boolean]

    C --> C1["type / interface"]
    C1 --> C2[Speaker]
    C1 --> C3[Props]
    C1 --> C4[State]

    D --> D1["useState<T>"]
    D --> D2["useReducer<S,A>"]
    D --> D3["createContext<T>"]
    D --> D4["useRef<T>"]

    E --> E1["boolean | undefined"]
    E --> E2["string | null"]
    E --> E3[Discriminated Union]
    E3 --> E3A["{ type: 'A'; payload: boolean }"]
    E3 --> E3B["{ type: 'B'; payload: string }"]
    E3 --> E3C["{ type: 'C' }"]

    C2 --> F[React Component Props]
    F --> F1["SpeakerDetailProps"]
    F --> F2["ThemeProviderProps"]
    F --> F3["LayoutProps - children: ReactNode"]

JS → TS Conversion Flow

flowchart LR
    A["File .jsx / .js"] -->|Rename| B["File .tsx / .ts"]
    B --> C{TypeScript errors?}
    C -->|Function parameters| D["Apply Pattern 1\nFunction Parameters"]
    C -->|Component props| E["Apply Pattern 2\nComponent Properties"]
    C -->|useState / useReducer\ncreateContext| F["Apply Pattern 3\nGeneric Types"]
    D --> G[Define type for each parameter]
    E --> H["Create TypeXxxProps\nAssign to props"]
    F --> I["useState<T>\ncreateContext<T>"]
    G --> J[✓ No TypeScript errors]
    H --> J
    I --> J

Zod Architecture in Next.js

sequenceDiagram
    participant Browser as Browser<br/>(Client Component)
    participant Server as Node.js<br/>(Server Component)
    participant DB as Database

    Server->>DB: loadSpeakersData()
    DB-->>Server: rawData (unverified)
    Server->>Server: speakerSchema.array().parse(rawData)
    Note over Server: Zod validation ✓<br/>Throws ZodError if invalid
    Server-->>Browser: Validated Speaker[] (serialized)
    Browser->>Browser: speakerSchema.array().parse(speakerList)
    Note over Browser: Re-validation Zod ✓<br/>Network boundary crossed

    Browser->>Server: speakerFavoriteToggleAction(speaker)
    Server->>Server: speakerSchema.parse(speakerRec)
    Note over Server: Entry validation ✓
    Server->>DB: updateSpeakerFavorite()
    DB-->>Server: OK
    Server-->>Browser: { success: true }

Reference Tables

Common React Types

React TypeDescriptionTypical usage
ReactNodeAnything React can render (JSX, string, null…)Component children
ReactElementA specific JSX element (result of React.createElement)Specific component return type
FC<Props>Function Component with typed propsconst Comp: FC<Props> = () => ...
Dispatch<A>Dispatch function from useReducerPassing dispatch as a prop
SetStateAction<S>Value or function passed to a useState setterTyping a useState setter
RefObject<T>Ref created with useRef(null) — tied to a DOM nodeuseRef<HTMLDivElement>(null)
MutableRefObject<T>Mutable ref — internal data (not DOM)useRef<number>(0)
CSSPropertiesReact inline style objectstyle: CSSProperties
ComponentProps<T>Infer props from an existing componenttype Btn = ComponentProps<'button'>

Typed React Events

EventTypeScript TypeAccess example
Click (button)React.MouseEvent<HTMLButtonElement>e.currentTarget
Click (div)React.MouseEvent<HTMLDivElement>e.preventDefault()
Change (input)React.ChangeEvent<HTMLInputElement>e.target.value
Change (select)React.ChangeEvent<HTMLSelectElement>e.target.value
Submit (form)React.FormEvent<HTMLFormElement>e.preventDefault()
KeyDownReact.KeyboardEvent<HTMLInputElement>e.key, e.code
FocusReact.FocusEvent<HTMLInputElement>e.target, e.relatedTarget
DragStartReact.DragEvent<HTMLDivElement>e.dataTransfer

Concrete examples:

// Handler for a search input
function SearchBar() {
  const [query, setQuery] = useState("");

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setQuery(e.target.value);
  }

  function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === "Escape") setQuery("");
  }

  return (
    <input
      type="text"
      value={query}
      onChange={handleChange}
      onKeyDown={handleKeyDown}
      placeholder="Search..."
    />
  );
}

// Handler for a form
function LoginForm() {
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    console.log(formData.get("email"));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" />
      <button type="submit">Sign In</button>
    </form>
  );
}

Common TypeScript Utilities

UtilityDescriptionExample
Partial<T>All properties of T become optionalPartial<Speaker> — for partial updates
Required<T>All properties of T become requiredRequired<Speaker>
Readonly<T>All properties of T become read-onlyReadonly<Speaker[]>
Pick<T, K>Select certain properties from TPick<Speaker, 'id' | 'firstName'>
Omit<T, K>Exclude certain properties from TOmit<Speaker, 'imageUrl'>
Record<K, V>Object with keys K and values VRecord<string, Speaker>
NonNullable<T>Exclude null and undefined from TNonNullable<string | null>
ReturnType<F>Return type of a functionReturnType<typeof useSpeakerSortAndFilter>
Parameters<F>Parameter types of a functionParameters<typeof fetch>
z.infer<typeof S>Infer the TypeScript type from a Zod schemaz.infer<typeof speakerSchema>

React Hooks and Their TypeScript Signatures

HookTypeScript SignatureNotes
useStateuseState<S>(initialState: S): [S, Dispatch<SetStateAction<S>>]Inference possible if initial value provided
useReduceruseReducer<R extends Reducer<any,any>>(reducer, initialState): [state, dispatch]State and Action typed via the reducer
useRef (DOM)useRef<T>(null): RefObject<T>T = DOM element type
useRef (mutable)useRef<T>(initialValue: T): MutableRefObject<T>For storing values without re-render
useContextuseContext<T>(context: Context<T>): TType inferred from createContext<T>
useCallbackuseCallback<T extends Function>(callback: T, deps): TType inferred from the callback
useMemouseMemo<T>(factory: () => T, deps): TType inferred from the factory
useEffectuseEffect(effect: () => void | (() => void), deps?): voidNo generic — always void
useTransitionuseTransition(): [boolean, TransitionStartFunction]isPending: boolean

Summary and Best Practices

flowchart TD
    A[React Project] --> B{New project?}
    B -->|Yes| C["npx create-next-app@latest\n→ Answer 'Yes' to TypeScript"]
    B -->|No - Migration| D[Rename files .js → .ts\n.jsx → .tsx]
    D --> E[Apply the 3 patterns\none file at a time]
    E --> F{Network data?}
    F -->|Yes| G[Add Zod\nnpm install zod]
    F -->|No| H[TypeScript alone is sufficient]
    G --> I[Create speakerSchema\nExport type via z.infer]
    I --> J[Validate: Server Component\nClient Component\nServer Function]
    C --> K[✓ Type-safe application]
    H --> K
    J --> K

JS → TS Migration Checklist:

  • tsconfig.json with "strict": true
  • Rename each file .jsx.tsx / .js.ts (one at a time)
  • Apply Pattern 1 (function parameters) to custom hooks
  • Apply Pattern 2 (component properties) to components
  • Apply Pattern 3 (generics) to useState, useReducer, createContext
  • Prefer initializing state values (avoids unnecessary undefined)
  • Use type for complex unions, interface for extensibility
  • For network data: install Zod and create schemas
  • Validate with Zod at both ends of every network boundary

Resources:


Search Terms

typescript · react · frontend · development · type · types · validation · component · pattern · server · zod · application · function · generic · hooks · reference · runtime · union · usereducer

Interested in this course?

Contact us to book it or get a custom training plan for your team.